diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index 8d0c1ae9..cdbf0985 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/breezyweather/BreezyWeatherSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/breezyweather/BreezyWeatherSettingsScreen.kt new file mode 100644 index 00000000..c427dc97 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/breezyweather/BreezyWeatherSettingsScreen.kt @@ -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) + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/breezyweather/BreezyWeatherSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/breezyweather/BreezyWeatherSettingsScreenVM.kt new file mode 100644 index 00000000..77f8705a --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/breezyweather/BreezyWeatherSettingsScreenVM.kt @@ -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() + } + ) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt index 1961f160..81f74798 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/integrations/IntegrationsSettingsScreen.kt @@ -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") + } + ) + } } } } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt index 626c1fdf..7aea5115 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreen.kt @@ -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), diff --git a/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt b/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt index a6c1451d..ff61acb0 100644 --- a/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt +++ b/core/base/src/main/java/de/mm20/launcher2/icons/Icons.kt @@ -1681,4 +1681,63 @@ private val _PrivateSpace = materialIcon("Icons.Rounded.PrivateSpace") { } val Icons.Rounded.PrivateSpace - get() = _PrivateSpace \ No newline at end of file + 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 \ No newline at end of file diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index bf7f5874..db77d1c5 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -345,6 +345,7 @@ Snow Hail Thunderstorm + Thunder Heavy snow showers Heavy rain showers Rain showers and thunder @@ -356,6 +357,7 @@ Heavy rain showers and thunder Fair Fog + Haze Sleet showers and thunder Rain and thunder Light sleet @@ -448,12 +450,14 @@ OpenWeatherMap Deutscher Wetterdienst (Germany only) HERE + Breezy Weather Location Automatic location Use GPS and location services to determine location automatically Managed by plugin The location for this provider is managed by the plugin app Location + Manage location in Breezy Weather Use degrees Fahrenheit and miles per hour Imperial units Compact mode @@ -671,6 +675,10 @@ 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. Tasks integration is fully set up and ready to use. Open Tasks app + Breezy Weather + 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. + 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 + Open Breezy Weather Tap to call Immediately start a call when tapping a phone number diff --git a/core/shared/src/main/java/de/mm20/launcher2/weather/WeatherIcon.kt b/core/shared/src/main/java/de/mm20/launcher2/weather/WeatherIcon.kt index 2d365627..ef190e88 100644 --- a/core/shared/src/main/java/de/mm20/launcher2/weather/WeatherIcon.kt +++ b/core/shared/src/main/java/de/mm20/launcher2/weather/WeatherIcon.kt @@ -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), } \ No newline at end of file diff --git a/data/weather/src/main/AndroidManifest.xml b/data/weather/src/main/AndroidManifest.xml index f4cc71dd..30d6a3a1 100644 --- a/data/weather/src/main/AndroidManifest.xml +++ b/data/weather/src/main/AndroidManifest.xml @@ -1,4 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/Module.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/Module.kt index 7c172808..31da3870 100644 --- a/data/weather/src/main/java/de/mm20/launcher2/weather/Module.kt +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/Module.kt @@ -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) } } diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherRepository.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherRepository.kt index fa14f177..146cbd11 100644 --- a/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherRepository.kt +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherRepository.kt @@ -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 @@ -71,13 +74,6 @@ internal class WeatherRepositoryImpl( } init { - val weatherRequest = - PeriodicWorkRequestBuilder(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(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 { diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherData.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherData.kt new file mode 100644 index 00000000..f5b7de2f --- /dev/null +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherData.kt @@ -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? = null, + val hourly: List? = 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, + ) +} \ No newline at end of file diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt new file mode 100644 index 00000000..659964cd --- /dev/null +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt @@ -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? { + // Noop implementation, because Breezy weather is handled in a special way + return null + } + + override suspend fun getWeatherData( + lat: Double, + lon: Double + ): List? { + // Noop implementation, because Breezy weather is handled in a special way + return null + } + + override suspend fun findLocation(query: String): List { + // 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() + + 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" + } +} \ No newline at end of file diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherReceiver.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherReceiver.kt new file mode 100644 index 00000000..91f2ce37 --- /dev/null +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherReceiver.kt @@ -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(weatherJson) + } catch (e: SerializationException) { + CrashReporter.logException(e) + return@launch + } + + BreezyWeatherProvider(context).pushWeatherData(weatherData) + + } + } +} \ No newline at end of file