Add Breezy Weather integration
This commit is contained in:
parent
4e2fbf965f
commit
5e59bf171a
@ -37,6 +37,7 @@ import de.mm20.launcher2.ui.overlays.OverlayHost
|
||||
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.backup.BackupSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.breezyweather.BreezyWeatherSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarProviderSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.calendarsearch.CalendarSearchSettingsScreen
|
||||
@ -244,6 +245,9 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/integrations/tasks") {
|
||||
TasksIntegrationSettingsScreen()
|
||||
}
|
||||
composable("settings/integrations/breezyweather") {
|
||||
BreezyWeatherSettingsScreen()
|
||||
}
|
||||
composable("settings/plugins") {
|
||||
PluginsSettingsScreen()
|
||||
}
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
package de.mm20.launcher2.ui.settings.breezyweather
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.OpenInNew
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
|
||||
@Composable
|
||||
fun BreezyWeatherSettingsScreen() {
|
||||
val activity = LocalActivity.current as AppCompatActivity
|
||||
val viewModel: BreezyWeatherSettingsScreenVM = viewModel()
|
||||
|
||||
val isBreezyInstalled by viewModel.isBreezyInstalled.collectAsStateWithLifecycle(null)
|
||||
val isWeatherProvider by viewModel.isBreezySetAsWeatherProvider.collectAsStateWithLifecycle(null)
|
||||
|
||||
PreferenceScreen(
|
||||
title = stringResource(R.string.preference_breezyweather_integration)
|
||||
) {
|
||||
if (isBreezyInstalled == false) {
|
||||
item {
|
||||
Banner(
|
||||
text = stringResource(
|
||||
R.string.preference_breezyweather_integration_description,
|
||||
stringResource(R.string.app_name),
|
||||
),
|
||||
icon = Icons.Rounded.Info,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
primaryAction = {
|
||||
Button(onClick = {
|
||||
viewModel.downloadBreezyApp(activity)
|
||||
}) {
|
||||
Text(stringResource(R.string.action_install))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isBreezyInstalled == true) {
|
||||
item {
|
||||
Banner(
|
||||
text = stringResource(
|
||||
R.string.preference_breezyweather_integration_instructions,
|
||||
stringResource(R.string.app_name),
|
||||
),
|
||||
icon = Icons.Rounded.Info,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceCategory {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_breezyweather_integration),
|
||||
summary = stringResource(
|
||||
if (isWeatherProvider == true) R.string.plugin_weather_provider_enabled
|
||||
else R.string.plugin_weather_provider_enable
|
||||
),
|
||||
enabled = isWeatherProvider == false,
|
||||
onClick = {
|
||||
viewModel.setBreezyAsWeatherProvider()
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_launch_breezyweather_app),
|
||||
icon = Icons.AutoMirrored.Rounded.OpenInNew,
|
||||
onClick = {
|
||||
viewModel.launchBreezyApp(activity)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package de.mm20.launcher2.ui.settings.breezyweather
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Process
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.applications.AppRepository
|
||||
import de.mm20.launcher2.preferences.weather.WeatherSettings
|
||||
import de.mm20.launcher2.weather.WeatherRepository
|
||||
import de.mm20.launcher2.weather.breezy.BreezyWeatherProvider
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class BreezyWeatherSettingsScreenVM: ViewModel(), KoinComponent {
|
||||
private val appRepository: AppRepository by inject()
|
||||
private val weatherSettings: WeatherSettings by inject()
|
||||
|
||||
private val breezyWeatherApp = appRepository.findOne("org.breezyweather", Process.myUserHandle())
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||
|
||||
val isBreezyInstalled = breezyWeatherApp
|
||||
.map { it != null }
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||
|
||||
val isBreezySetAsWeatherProvider = weatherSettings.providerId
|
||||
.map { it == BreezyWeatherProvider.Id }
|
||||
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
|
||||
|
||||
fun setBreezyAsWeatherProvider() {
|
||||
weatherSettings.setProvider(BreezyWeatherProvider.Id)
|
||||
}
|
||||
|
||||
fun launchBreezyApp(activity: AppCompatActivity) {
|
||||
viewModelScope.launch {
|
||||
breezyWeatherApp.first()?.launch(activity, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadBreezyApp(activity: AppCompatActivity) {
|
||||
activity.startActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
data = "https://github.com/breezy-weather/breezy-weather/blob/main/INSTALL.md".toUri()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -7,11 +7,13 @@ import androidx.compose.material.icons.rounded.PlayCircleOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.icons.BreezyWeather
|
||||
import de.mm20.launcher2.icons.Google
|
||||
import de.mm20.launcher2.icons.Nextcloud
|
||||
import de.mm20.launcher2.icons.Owncloud
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||
|
||||
@ -22,41 +24,54 @@ fun IntegrationsSettingsScreen() {
|
||||
|
||||
PreferenceScreen(title = stringResource(R.string.preference_screen_integrations)) {
|
||||
item {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_weather_integration),
|
||||
icon = Icons.Rounded.LightMode,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/weather")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_media_integration),
|
||||
icon = Icons.Rounded.PlayCircleOutline,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/media")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_nextcloud),
|
||||
icon = Icons.Rounded.Nextcloud,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/nextcloud")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_owncloud),
|
||||
icon = Icons.Rounded.Owncloud,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/owncloud")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_tasks_integration),
|
||||
icon = Icons.Default.Check,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/tasks")
|
||||
}
|
||||
)
|
||||
PreferenceCategory {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_weather_integration),
|
||||
icon = Icons.Rounded.LightMode,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/weather")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_media_integration),
|
||||
icon = Icons.Rounded.PlayCircleOutline,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/media")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
PreferenceCategory {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_nextcloud),
|
||||
icon = Icons.Rounded.Nextcloud,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/nextcloud")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_owncloud),
|
||||
icon = Icons.Rounded.Owncloud,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/owncloud")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_tasks_integration),
|
||||
icon = Icons.Default.Check,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/tasks")
|
||||
}
|
||||
)
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_breezyweather_integration),
|
||||
icon = Icons.Rounded.BreezyWeather,
|
||||
onClick = {
|
||||
navController?.navigate("settings/integrations/breezyweather")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package de.mm20.launcher2.ui.settings.weather
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -18,6 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.sendWithBackgroundPermission
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.plugin.PluginState
|
||||
import de.mm20.launcher2.ui.BuildConfig
|
||||
import de.mm20.launcher2.ui.R
|
||||
@ -26,6 +28,7 @@ import de.mm20.launcher2.ui.component.Banner
|
||||
import de.mm20.launcher2.ui.component.MissingPermissionBanner
|
||||
import de.mm20.launcher2.ui.component.preferences.*
|
||||
import de.mm20.launcher2.weather.WeatherProviderInfo
|
||||
import de.mm20.launcher2.weather.breezy.BreezyWeatherProvider
|
||||
|
||||
@Composable
|
||||
fun WeatherIntegrationSettingsScreen() {
|
||||
@ -93,7 +96,16 @@ fun WeatherIntegrationSettingsScreen() {
|
||||
}
|
||||
item {
|
||||
PreferenceCategory(title = stringResource(R.string.preference_category_location)) {
|
||||
if (selectedProviderInfo?.managedLocation == true) {
|
||||
if (selectedProviderInfo?.id == BreezyWeatherProvider.Id) {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_location),
|
||||
summary = stringResource(R.string.preference_location_breezy),
|
||||
onClick = {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage("org.breezyweather") ?: return@Preference
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
)
|
||||
} else if (selectedProviderInfo?.managedLocation == true) {
|
||||
Preference(
|
||||
title = stringResource(R.string.preference_location_managed),
|
||||
summary = stringResource(R.string.preference_location_managed_summary),
|
||||
|
||||
@ -1682,3 +1682,62 @@ private val _PrivateSpace = materialIcon("Icons.Rounded.PrivateSpace") {
|
||||
|
||||
val Icons.Rounded.PrivateSpace
|
||||
get() = _PrivateSpace
|
||||
|
||||
private val _BreezyWeather = materialIcon("Icons.Rounded.BreezyWeather") {
|
||||
materialPath {
|
||||
moveTo(11.51596f, 15.483644f)
|
||||
curveToRelative(-0.270954f, 0.445157f, -0.619347f, 0.870954f, -1.006439f, 1.258046f)
|
||||
curveToRelative(-0.5806338f, 0.580637f, -1.1999812f, 1.02579f, -1.8967447f, 1.354821f)
|
||||
curveToRelative(0.8709544f, 0.483862f, 1.8580387f, 0.793534f, 2.9031837f, 0.851599f)
|
||||
close()
|
||||
moveTo(8.5160015f, 11.51596f)
|
||||
curveTo(8.0708487f, 11.245006f, 7.6450471f, 10.896616f, 7.2579555f, 10.509525f)
|
||||
curveTo(6.6966767f, 9.9482422f, 6.2515202f, 9.3288948f, 5.9224934f, 8.6514863f)
|
||||
curveTo(5.4386307f, 9.5224406f, 5.1483101f, 10.49017f, 5.070894f, 11.535315f)
|
||||
horizontalLineToRelative(3.4451075f)
|
||||
close()
|
||||
moveToRelative(6.9676425f, 0.967729f)
|
||||
curveToRelative(0.445153f, 0.270955f, 0.870954f, 0.619344f, 1.258046f, 1.006436f)
|
||||
curveToRelative(0.580634f, 0.580637f, 1.02579f, 1.199984f, 1.354817f, 1.877393f)
|
||||
curveToRelative(0.483863f, -0.870954f, 0.774184f, -1.858038f, 0.8516f, -2.903184f)
|
||||
horizontalLineTo(15.483644f)
|
||||
close()
|
||||
moveTo(12.483685f, 8.5160053f)
|
||||
curveTo(12.75464f, 8.0708488f, 13.103033f, 7.6450509f, 13.490125f, 7.2579593f)
|
||||
curveTo(14.051407f, 6.6966767f, 14.670751f, 6.251524f, 15.348159f, 5.9224934f)
|
||||
curveTo(14.49656f, 5.4579857f, 13.509479f, 5.1483139f, 12.483685f, 5.090249f)
|
||||
close()
|
||||
moveTo(5.7483025f, 17.76748f)
|
||||
curveToRelative(1.6064278f, 0f, 2.9225387f, -0.561279f, 4.0644549f, -1.703199f)
|
||||
curveToRelative(1.0257906f, -1.02579f, 1.5677176f, -2.20642f, 1.6838476f, -3.580592f)
|
||||
horizontalLineTo(0f)
|
||||
curveToRelative(0.096756f, 1.393527f, 0.6580535f, 2.554802f, 1.6838476f, 3.580592f)
|
||||
curveToRelative(1.1419162f, 1.14192f, 2.4580271f, 1.703199f, 4.0644549f, 1.703199f)
|
||||
close()
|
||||
moveTo(7.9353678f, 9.8127613f)
|
||||
curveTo(8.961158f, 10.838552f, 10.141784f, 11.380479f, 11.51596f, 11.496605f)
|
||||
verticalLineTo(0f)
|
||||
curveTo(10.122429f, 0.096756f, 8.961158f, 0.6580573f, 7.9353678f, 1.6838476f)
|
||||
curveTo(6.7934477f, 2.8257677f, 6.2321652f, 4.1612297f, 6.2321652f, 5.7483025f)
|
||||
curveToRelative(0f, 1.5870766f, 0.5612825f, 2.9225387f, 1.7032026f, 4.0644588f)
|
||||
close()
|
||||
moveToRelative(4.5483172f, 2.6902827f)
|
||||
verticalLineToRelative(11.51596f)
|
||||
curveToRelative(1.393531f, -0.09676f, 2.593512f, -0.677412f, 3.580593f, -1.683848f)
|
||||
curveToRelative(1.14192f, -1.14192f, 1.703202f, -2.477382f, 1.703202f, -4.083809f)
|
||||
curveToRelative(0f, -1.606428f, -0.561282f, -2.922539f, -1.703202f, -4.064459f)
|
||||
curveToRelative(-1.025791f, -1.02579f, -2.206417f, -1.587073f, -3.580593f, -1.683844f)
|
||||
close()
|
||||
moveToRelative(5.767658f, -6.270875f)
|
||||
curveToRelative(-1.606428f, 0f, -2.922539f, 0.5612825f, -4.064455f, 1.7031988f)
|
||||
curveTo(13.161098f, 8.9611581f, 12.61917f, 10.141788f, 12.50304f, 11.51596f)
|
||||
horizontalLineTo(23.999646f)
|
||||
curveTo(23.90289f, 10.122433f, 23.341592f, 8.9611581f, 22.315798f, 7.9353678f)
|
||||
curveTo(21.173882f, 6.7934515f, 19.838416f, 6.232169f, 18.251343f, 6.232169f)
|
||||
close()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val Icons.Rounded.BreezyWeather
|
||||
get() = _BreezyWeather
|
||||
@ -345,6 +345,7 @@
|
||||
<string name="weather_condition_snow">Snow</string>
|
||||
<string name="weather_condition_hail">Hail</string>
|
||||
<string name="weather_condition_thunderstorm">Thunderstorm</string>
|
||||
<string name="weather_condition_thunder">Thunder</string>
|
||||
<string name="weather_condition_heavysnowshowers">Heavy snow showers</string>
|
||||
<string name="weather_condition_heavyrainshowers">Heavy rain showers</string>
|
||||
<string name="weather_condition_rainshowersandthunder">Rain showers and thunder</string>
|
||||
@ -356,6 +357,7 @@
|
||||
<string name="weather_condition_heavyrainshowersandthunder">Heavy rain showers and thunder</string>
|
||||
<string name="weather_condition_fair">Fair</string>
|
||||
<string name="weather_condition_fog">Fog</string>
|
||||
<string name="weather_condition_haze">Haze</string>
|
||||
<string name="weather_condition_sleetshowersandthunder">Sleet showers and thunder</string>
|
||||
<string name="weather_condition_rainandthunder">Rain and thunder</string>
|
||||
<string name="weather_condition_lightsleet">Light sleet</string>
|
||||
@ -448,12 +450,14 @@
|
||||
<string name="provider_openweathermap">OpenWeatherMap</string>
|
||||
<string name="provider_brightsky">Deutscher Wetterdienst (Germany only)</string>
|
||||
<string name="provider_here">HERE</string>
|
||||
<string name="provider_breezy">Breezy Weather</string>
|
||||
<string name="preference_category_location">Location</string>
|
||||
<string name="preference_automatic_location">Automatic location</string>
|
||||
<string name="preference_automatic_location_summary">Use GPS and location services to determine location automatically</string>
|
||||
<string name="preference_location_managed">Managed by plugin</string>
|
||||
<string name="preference_location_managed_summary">The location for this provider is managed by the plugin app</string>
|
||||
<string name="preference_location">Location</string>
|
||||
<string name="preference_location_breezy">Manage location in Breezy Weather</string>
|
||||
<string name="preference_imperial_units_summary">Use degrees Fahrenheit and miles per hour</string>
|
||||
<string name="preference_imperial_units">Imperial units</string>
|
||||
<string name="widget_config_weather_compact">Compact mode</string>
|
||||
@ -671,6 +675,10 @@
|
||||
<string name="preference_tasks_integration_description">Tasks is a free and open source to-do list and reminders app. If installed, %1$s can display and search your tasks from the Tasks app.</string>
|
||||
<string name="preference_tasks_integration_ready">Tasks integration is fully set up and ready to use.</string>
|
||||
<string name="preference_launch_tasks_app">Open Tasks app</string>
|
||||
<string name="preference_breezyweather_integration">Breezy Weather</string>
|
||||
<string name="preference_breezyweather_integration_description">Breezy Weather is a free and open source weather app. If installed, %1$s can use Breezy Weather as a data source for weather data.</string>
|
||||
<string name="preference_breezyweather_integration_instructions">To use Breezy Weather as a weather provider:\n\n1. Go to Breezy Weather > Settings > Widgets & Live wallpaper > Send Gadgetbridge data > Enable %1$s\n\n2. In %1$s, select Breezy Weather as weather provider</string>
|
||||
<string name="preference_launch_breezyweather_app">Open Breezy Weather</string>
|
||||
<string name="preference_contacts_call_on_tap">Tap to call</string>
|
||||
<string name="preference_contacts_call_on_tap_summary">Immediately start a call when tapping a phone number</string>
|
||||
<!-- Used in an info banner if a specific feature requires a Nextcloud account -->
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
enum class WeatherIcon {
|
||||
Unknown,
|
||||
Clear,
|
||||
Cloudy,
|
||||
Cold,
|
||||
Drizzle,
|
||||
Haze,
|
||||
Fog,
|
||||
Hail,
|
||||
HeavyThunderstorm,
|
||||
HeavyThunderstormWithRain,
|
||||
Hot,
|
||||
MostlyCloudy,
|
||||
PartlyCloudy,
|
||||
Showers,
|
||||
Sleet,
|
||||
Snow,
|
||||
Storm,
|
||||
Thunderstorm,
|
||||
ThunderstormWithRain,
|
||||
Wind,
|
||||
BrokenClouds,
|
||||
enum class WeatherIcon(val id: Int) {
|
||||
Unknown(-1),
|
||||
Clear(0),
|
||||
Cloudy(1),
|
||||
Cold(2),
|
||||
Drizzle(3),
|
||||
Haze(4),
|
||||
Fog(5),
|
||||
Hail(6),
|
||||
HeavyThunderstorm(7),
|
||||
HeavyThunderstormWithRain(8),
|
||||
Hot(9),
|
||||
MostlyCloudy(10),
|
||||
PartlyCloudy(11),
|
||||
Showers(12),
|
||||
Sleet(13),
|
||||
Snow(14),
|
||||
Storm(15),
|
||||
Thunderstorm(16),
|
||||
ThunderstormWithRain(17),
|
||||
Wind(18),
|
||||
BrokenClouds(19),
|
||||
}
|
||||
@ -1,4 +1,12 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<application>
|
||||
<receiver android:name=".breezy.BreezyWeatherReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="nodomain.freeyourgadget.gadgetbridge.ACTION_GENERIC_WEATHER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
@ -1,5 +1,6 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
import de.mm20.launcher2.weather.breezy.BreezyWeatherProvider
|
||||
import de.mm20.launcher2.weather.brightsky.BrightSkyProvider
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
@ -17,6 +18,7 @@ val weatherModule = module {
|
||||
MetNoProvider.Id -> MetNoProvider(androidContext(), get())
|
||||
HereProvider.Id -> HereProvider(androidContext())
|
||||
BrightSkyProvider.Id -> BrightSkyProvider(androidContext())
|
||||
BreezyWeatherProvider.Id -> BreezyWeatherProvider(androidContext())
|
||||
else -> PluginWeatherProvider(androidContext(), providerId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.preferences.LatLon
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.preferences.weather.WeatherSettings
|
||||
import de.mm20.launcher2.weather.breezy.BreezyWeatherProvider
|
||||
import de.mm20.launcher2.weather.brightsky.BrightSkyProvider
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
@ -24,7 +25,9 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
interface WeatherRepository {
|
||||
fun getActiveProvider(): Flow<WeatherProviderInfo?>
|
||||
@ -71,13 +74,6 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
|
||||
init {
|
||||
val weatherRequest =
|
||||
PeriodicWorkRequestBuilder<WeatherUpdateWorker>(Duration.ofMinutes(60))
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
"weather",
|
||||
ExistingPeriodicWorkPolicy.UPDATE, weatherRequest
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
hasLocationPermission.collectLatest {
|
||||
@ -86,6 +82,14 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
scope.launch {
|
||||
settings.collectLatest {
|
||||
val provider = WeatherProvider.getInstance(it.provider)
|
||||
val weatherRequest =
|
||||
PeriodicWorkRequestBuilder<WeatherUpdateWorker>(Duration.ofMillis(provider.getUpdateInterval()))
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
"weather",
|
||||
ExistingPeriodicWorkPolicy.UPDATE, weatherRequest
|
||||
)
|
||||
requestUpdate()
|
||||
}
|
||||
}
|
||||
@ -188,6 +192,15 @@ internal class WeatherRepositoryImpl(
|
||||
)
|
||||
)
|
||||
}
|
||||
if (BreezyWeatherProvider.isAvailable(context)) {
|
||||
providers.add(
|
||||
WeatherProviderInfo(
|
||||
BreezyWeatherProvider.Id,
|
||||
context.getString(R.string.provider_breezy),
|
||||
managedLocation = true
|
||||
)
|
||||
)
|
||||
}
|
||||
val pluginProviders = pluginRepository.findMany(type = PluginType.Weather, enabled = true)
|
||||
return pluginProviders.map {
|
||||
providers + it.mapNotNull {
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
package de.mm20.launcher2.weather.breezy
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class BreezyWeatherData(
|
||||
val timestamp: Long? = null,
|
||||
val location: String? = null,
|
||||
val currentTemp: Double? = null,
|
||||
/**
|
||||
* According to the spec this can be any OWM weather code (see https://openweathermap.org/weather-conditions),
|
||||
* but in reality, only the following codes are ever used:
|
||||
* 800, 801, 803, 500, 600, 771, 741, 751, 611, 511, 210, 211, 3200
|
||||
* (see https://github.com/breezy-weather/breezy-weather/blob/main/app/src/main/java/org/breezyweather/sources/gadgetbridge/GadgetbridgeService.kt#L37)
|
||||
*/
|
||||
val currentConditionCode: Int? = null,
|
||||
val currentCondition: String? = null,
|
||||
val currentHumidity: Int? = null,
|
||||
val todayMaxTemp: Int? = null,
|
||||
val todayMinTemp: Int? = null,
|
||||
val windSpeed: Float? = null,
|
||||
val windDirection: Int? = null,
|
||||
val uvIndex: Float? = null,
|
||||
val precipProbability: Int? = null,
|
||||
val dewPoint: Int? = null,
|
||||
val pressure: Float? = null,
|
||||
val cloudCover: Int? = null,
|
||||
val visibility: Float? = null,
|
||||
val sunRise: Int? = null,
|
||||
val sunSet: Int? = null,
|
||||
val moonRise: Int? = null,
|
||||
val moonSet: Int? = null,
|
||||
val moonPhase: Int? = null,
|
||||
val feelsLikeTemp: Int? = null,
|
||||
val forecasts: List<DailyForecast>? = null,
|
||||
val hourly: List<HourlyForecast>? = null,
|
||||
val airQuality: AirQuality? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class AirQuality(
|
||||
val aqi: Int? = null,
|
||||
val co: Float? = null,
|
||||
val no2: Float? = null,
|
||||
val o3: Float? = null,
|
||||
val pm10: Float? = null,
|
||||
val pm25: Float? = null,
|
||||
val so2: Float? = null,
|
||||
val coAqi: Int? = null,
|
||||
val no2Aqi: Int? = null,
|
||||
val o3Aqi: Int? = null,
|
||||
val pm10Aqi: Int? = null,
|
||||
val pm25Aqi: Int? = null,
|
||||
val so2Aqi: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DailyForecast(
|
||||
val minTemp: Int? = null,
|
||||
val maxTemp: Int? = null,
|
||||
val conditionCode: Int? = null,
|
||||
val humidity: Int? = null,
|
||||
val windSpeed: Float? = null,
|
||||
val windDirection: Int? = null,
|
||||
val uvIndex: Float? = null,
|
||||
val precipProbability: Int? = null,
|
||||
val sunRise: Int? = null,
|
||||
val sunSet: Int? = null,
|
||||
val moonRise: Int? = null,
|
||||
val moonSet: Int? = null,
|
||||
val moonPhase: Int? = null,
|
||||
val airQuality: AirQuality? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HourlyForecast(
|
||||
val timestamp: Int? = null,
|
||||
val temp: Int? = null,
|
||||
val conditionCode: Int? = null,
|
||||
val humidity: Int? = null,
|
||||
val windSpeed: Float? = null,
|
||||
val windDirection: Int? = null,
|
||||
val uvIndex: Float? = null,
|
||||
val precipProbability: Int? = null,
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
package de.mm20.launcher2.weather.breezy
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.preferences.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherIcon
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class BreezyWeatherProvider(
|
||||
private val context: Context,
|
||||
) : WeatherProvider, KoinComponent {
|
||||
private val database: AppDatabase by inject()
|
||||
|
||||
override suspend fun getWeatherData(location: WeatherLocation): List<Forecast>? {
|
||||
// Noop implementation, because Breezy weather is handled in a special way
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun getWeatherData(
|
||||
lat: Double,
|
||||
lon: Double
|
||||
): List<Forecast>? {
|
||||
// Noop implementation, because Breezy weather is handled in a special way
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun findLocation(query: String): List<WeatherLocation> {
|
||||
// Noop implementation, because Breezy weather is handled in a special way
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun getUpdateInterval(): Long {
|
||||
// Updates are pushed, no need to pull
|
||||
return Long.MAX_VALUE
|
||||
}
|
||||
|
||||
internal suspend fun pushWeatherData(data: BreezyWeatherData) {
|
||||
val result = mutableListOf<Forecast>()
|
||||
|
||||
val lastUpdate = System.currentTimeMillis()
|
||||
|
||||
result += Forecast(
|
||||
timestamp = data.timestamp?.times(1000L) ?: return,
|
||||
temperature = data.currentTemp ?: return,
|
||||
icon = iconForId(data.currentConditionCode ?: return).id,
|
||||
condition = data.currentCondition ?: return,
|
||||
location = data.location ?: return,
|
||||
provider = "Breezy Weather",
|
||||
clouds = data.cloudCover,
|
||||
humidity = data.currentHumidity?.toDouble(),
|
||||
pressure = data.pressure?.toDouble(),
|
||||
windSpeed = data.windSpeed?.toDouble()?.div(3.6),
|
||||
precipProbability = data.precipProbability,
|
||||
windDirection = data.windDirection?.toDouble(),
|
||||
providerUrl = "de.mm20.launcher.plugin.breezyweather://-",
|
||||
night = isNight(
|
||||
data.timestamp.times(1000L),
|
||||
data.sunRise?.times(1000L),
|
||||
data.sunSet?.times(1000L)
|
||||
),
|
||||
updateTime = lastUpdate,
|
||||
)
|
||||
|
||||
val sunrises = buildList {
|
||||
if (data.sunRise != null) add(data.sunRise.times(1000L))
|
||||
if (data.forecasts != null) addAll(data.forecasts.mapNotNull { it.sunRise?.times(1000L) })
|
||||
}.sorted()
|
||||
|
||||
val sunsets = buildList {
|
||||
if (data.sunSet != null) add(data.sunSet.times(1000L))
|
||||
if (data.forecasts != null) addAll(data.forecasts.mapNotNull { it.sunSet?.times(1000L) })
|
||||
}.sorted()
|
||||
|
||||
|
||||
for (hourly in data.hourly ?: emptyList()) {
|
||||
val timestamp = hourly.timestamp?.times(1000L) ?: continue
|
||||
|
||||
val lastSunrise = sunrises.findLast { it < timestamp }
|
||||
val lastSunset = sunsets.findLast { it < timestamp }
|
||||
val nextSunrise = sunrises.find { it > timestamp }
|
||||
val nextSunset = sunsets.find { it > timestamp }
|
||||
|
||||
val isNight = when {
|
||||
lastSunrise != null && lastSunset != null -> lastSunrise < lastSunset
|
||||
nextSunrise != null && nextSunset != null -> nextSunrise < nextSunset
|
||||
lastSunset != null && lastSunrise == null -> true
|
||||
nextSunrise != null && nextSunset == null -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
result += Forecast(
|
||||
timestamp = timestamp,
|
||||
temperature = hourly.temp?.toDouble() ?: continue,
|
||||
icon = iconForId(hourly.conditionCode ?: continue).id,
|
||||
condition = textForId(hourly.conditionCode) ?: continue,
|
||||
location = data.location,
|
||||
provider = "Breezy Weather",
|
||||
humidity = hourly.humidity?.toDouble(),
|
||||
windSpeed = hourly.windSpeed?.toDouble()?.div(3.6),
|
||||
precipProbability = hourly.precipProbability,
|
||||
windDirection = hourly.windDirection?.toDouble(),
|
||||
updateTime = lastUpdate,
|
||||
providerUrl = "de.mm20.launcher.plugin.breezyweather://-",
|
||||
night = isNight
|
||||
)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
database.weatherDao()
|
||||
.replaceAll(result.map { it.toDatabaseEntity() })
|
||||
}
|
||||
}
|
||||
|
||||
private fun iconForId(id: Int): WeatherIcon {
|
||||
return when (id) {
|
||||
200, 201, in 230..232 -> WeatherIcon.ThunderstormWithRain
|
||||
202 -> WeatherIcon.ThunderstormWithRain
|
||||
210, 211 -> WeatherIcon.Thunderstorm
|
||||
212, 221 -> WeatherIcon.HeavyThunderstorm
|
||||
in 300..302, in 310..312 -> WeatherIcon.Drizzle
|
||||
313, 314, 321, in 500..504, 511, in 520..522, 531 -> WeatherIcon.Showers
|
||||
in 600..602 -> WeatherIcon.Snow
|
||||
611, 612, 615, 616, in 620..622 -> WeatherIcon.Sleet
|
||||
701, 711, 731, 741, 761, 762 -> WeatherIcon.Fog
|
||||
721 -> WeatherIcon.Haze
|
||||
771, 781, in 900..902, in 958..962 -> WeatherIcon.Storm
|
||||
800 -> WeatherIcon.Clear
|
||||
801 -> WeatherIcon.PartlyCloudy
|
||||
802 -> WeatherIcon.MostlyCloudy
|
||||
803 -> WeatherIcon.BrokenClouds
|
||||
804, 951 -> WeatherIcon.Cloudy
|
||||
903 -> WeatherIcon.Cold
|
||||
904 -> WeatherIcon.Hot
|
||||
905, in 952..957 -> WeatherIcon.Wind
|
||||
906 -> WeatherIcon.Hail
|
||||
else -> WeatherIcon.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun textForId(id: Int): String? {
|
||||
val resId = when (id) {
|
||||
800 -> R.string.weather_condition_clearsky
|
||||
801 -> R.string.weather_condition_partlycloudy
|
||||
803 -> R.string.weather_condition_cloudy
|
||||
500 -> R.string.weather_condition_rain
|
||||
600 -> R.string.weather_condition_snow
|
||||
771 -> R.string.weather_condition_wind
|
||||
741 -> R.string.weather_condition_fog
|
||||
751 -> R.string.weather_condition_haze
|
||||
611 -> R.string.weather_condition_sleet
|
||||
511 -> R.string.weather_condition_hail
|
||||
210 -> R.string.weather_condition_thunder
|
||||
211 -> R.string.weather_condition_thunderstorm
|
||||
else -> R.string.weather_condition_unknown
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
private fun isNight(timestamp: Long, sunrise: Long?, sunset: Long?): Boolean {
|
||||
return (sunrise != null && timestamp < sunrise) || (sunset != null && timestamp > sunset)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal fun isAvailable(context: Context): Boolean {
|
||||
return try {
|
||||
context.packageManager.getPackageInfo("org.breezyweather", 0)
|
||||
return true
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const val Id = "breezy"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package de.mm20.launcher2.weather.breezy
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.weather.WeatherSettings
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerializationException
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class BreezyWeatherReceiver: BroadcastReceiver(), KoinComponent {
|
||||
|
||||
private val settings: WeatherSettings by inject()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
scope.launch {
|
||||
val provider = settings.providerId.first()
|
||||
if (provider != BreezyWeatherProvider.Id) {
|
||||
return@launch
|
||||
}
|
||||
val weatherJson = intent.getStringExtra("WeatherJson")
|
||||
|
||||
if (weatherJson == null) {
|
||||
Log.e("BreezyWeatherPlugin", "Broadcast was received but WeatherJson was null")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val weatherData = try {
|
||||
Json.Lenient.decodeFromString<BreezyWeatherData>(weatherJson)
|
||||
} catch (e: SerializationException) {
|
||||
CrashReporter.logException(e)
|
||||
return@launch
|
||||
}
|
||||
|
||||
BreezyWeatherProvider(context).pushWeatherData(weatherData)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user