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