From 1d6688831c2e84ee17164358f45575660895147a Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Fri, 3 May 2024 13:02:17 +0200 Subject: [PATCH] Add managed location to weather plugin API --- .../WeatherIntegrationSettingsScreen.kt | 60 +++++++++------- .../WeatherIntegrationSettingsScreenVM.kt | 4 +- .../de/mm20/launcher2/plugin/PluginApi.kt | 51 +++++++++++++ .../plugin/config/WeatherPluginConfig.kt | 1 + core/i18n/src/main/res/values/strings.xml | 2 + .../preferences/LauncherSettingsData.kt | 1 + .../preferences/weather/WeatherSettings.kt | 31 +++++++- .../plugin/config/WeatherPluginConfig.kt | 7 +- .../files/providers/PluginFileProvider.kt | 19 +---- .../launcher2/weather/WeatherProviderInfo.kt | 1 + .../launcher2/weather/WeatherRepository.kt | 6 +- .../weather/plugin/PluginWeatherProvider.kt | 27 +++---- .../common/_search_plugin_config.md | 21 +++++- .../plugins/plugin-types/file-search.md | 40 ++++++++--- .../plugins/plugin-types/weather.md | 72 ++++++++++++++----- gradle/libs.versions.toml | 2 +- .../sdk/config/WeatherPluginConfig.kt | 1 + .../launcher2/sdk/weather/WeatherLocation.kt | 4 ++ .../launcher2/sdk/weather/WeatherProvider.kt | 6 ++ 19 files changed, 256 insertions(+), 100 deletions(-) create mode 100644 core/base/src/main/java/de/mm20/launcher2/plugin/PluginApi.kt 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 83807200..4198ce7a 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 @@ -32,6 +32,11 @@ fun WeatherIntegrationSettingsScreen() { val context = LocalContext.current val availableProviders by viewModel.availableProviders.collectAsState(emptyList()) + val weatherProvider by viewModel.weatherProvider.collectAsState() + + val selectedProviderInfo by remember { + derivedStateOf { availableProviders.find { it.id == weatherProvider } } + } val pluginState by viewModel.weatherProviderPluginState.collectAsStateWithLifecycle( null, @@ -63,7 +68,6 @@ fun WeatherIntegrationSettingsScreen() { } ) } - val weatherProvider by viewModel.weatherProvider.collectAsState() ListPreference( title = stringResource(R.string.preference_weather_provider), items = availableProviders.map{ @@ -88,31 +92,39 @@ fun WeatherIntegrationSettingsScreen() { } item { PreferenceCategory(title = stringResource(R.string.preference_category_location)) { - val hasPermission by viewModel.hasLocationPermission.collectAsState() - AnimatedVisibility(hasPermission == false) { - MissingPermissionBanner( - text = stringResource(R.string.missing_permission_auto_location), - onClick = { - viewModel.requestLocationPermission(context as AppCompatActivity) - }, - modifier = Modifier.padding(16.dp) + if (selectedProviderInfo?.managedLocation == true) { + Preference( + title = stringResource(R.string.preference_location_managed), + summary = stringResource(R.string.preference_location_managed_summary), + enabled = false + ) + } else { + val hasPermission by viewModel.hasLocationPermission.collectAsState() + AnimatedVisibility(hasPermission == false) { + MissingPermissionBanner( + text = stringResource(R.string.missing_permission_auto_location), + onClick = { + viewModel.requestLocationPermission(context as AppCompatActivity) + }, + modifier = Modifier.padding(16.dp) + ) + } + val autoLocation by viewModel.autoLocation.collectAsState() + SwitchPreference( + title = stringResource(R.string.preference_automatic_location), + summary = stringResource(R.string.preference_automatic_location_summary), + value = autoLocation, + onValueChanged = { + viewModel.setAutoLocation(it) + } + ) + val location by viewModel.location.collectAsStateWithLifecycle() + LocationPreference( + title = stringResource(R.string.preference_location), + value = location, + enabled = !autoLocation, ) } - val autoLocation by viewModel.autoLocation.collectAsState() - SwitchPreference( - title = stringResource(R.string.preference_automatic_location), - summary = stringResource(R.string.preference_automatic_location_summary), - value = autoLocation, - onValueChanged = { - viewModel.setAutoLocation(it) - } - ) - val location by viewModel.location.collectAsStateWithLifecycle() - LocationPreference( - title = stringResource(R.string.preference_location), - value = location, - enabled = !autoLocation, - ) } } if (BuildConfig.DEBUG) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt index 26d1f409..c8173380 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/weather/WeatherIntegrationSettingsScreenVM.kt @@ -8,7 +8,6 @@ import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.plugins.PluginService import de.mm20.launcher2.preferences.weather.WeatherSettings import de.mm20.launcher2.weather.WeatherRepository -import kotlinx.coroutines.* import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -27,6 +26,7 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { val weatherProvider = weatherSettings.providerId .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + fun setWeatherProvider(provider: String) { weatherSettings.setProvider(provider) } @@ -42,6 +42,7 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { val autoLocation = weatherSettings.autoLocation .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + fun setAutoLocation(autoLocation: Boolean) { weatherSettings.setAutoLocation(autoLocation) } @@ -61,7 +62,6 @@ class WeatherIntegrationSettingsScreenVM : ViewModel(), KoinComponent { permissionsManager.requestPermission(activity, PermissionGroup.Location) } - fun clearWeatherData() { repository.deleteForecasts() } diff --git a/core/base/src/main/java/de/mm20/launcher2/plugin/PluginApi.kt b/core/base/src/main/java/de/mm20/launcher2/plugin/PluginApi.kt new file mode 100644 index 00000000..98d85471 --- /dev/null +++ b/core/base/src/main/java/de/mm20/launcher2/plugin/PluginApi.kt @@ -0,0 +1,51 @@ +package de.mm20.launcher2.plugin + +import android.content.ContentResolver +import android.net.Uri +import android.util.Log +import de.mm20.launcher2.plugin.config.SearchPluginConfig +import de.mm20.launcher2.plugin.config.WeatherPluginConfig +import de.mm20.launcher2.plugin.contracts.PluginContract + +class PluginApi( + private val pluginAuthority: String, + private val contentResolver: ContentResolver, +) { + fun getSearchPluginConfig(): SearchPluginConfig? { + val configBundle = try { + contentResolver.call( + Uri.Builder() + .scheme("content") + .authority(pluginAuthority) + .build(), + PluginContract.Methods.GetConfig, + null, + null + ) ?: return null + } catch (e: Exception) { + Log.e("MM20", "Plugin $pluginAuthority threw exception", e) + return null + } + + return SearchPluginConfig(configBundle) + } + + fun getWeatherPluginConfig(): WeatherPluginConfig? { + val configBundle = try { + contentResolver.call( + Uri.Builder() + .scheme("content") + .authority(pluginAuthority) + .build(), + PluginContract.Methods.GetConfig, + null, + null + ) ?: return null + } catch (e: Exception) { + Log.e("PluginWeatherProvider", "Plugin $pluginAuthority threw exception", e) + return null + } + + return WeatherPluginConfig(configBundle) + } +} \ No newline at end of file diff --git a/core/base/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt b/core/base/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt index 7d125333..ceded3e7 100644 --- a/core/base/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt +++ b/core/base/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt @@ -8,5 +8,6 @@ fun WeatherPluginConfig(bundle: Bundle): WeatherPluginConfig { "minUpdateInterval", 60 * 60 * 1000L ), + managedLocation = bundle.getBoolean("managedLocation", false) ) } \ 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 b8c457ab..08e052c0 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -481,6 +481,8 @@ Location Automatic location Use GPS and location services to determine location automatically + Managed by plugin + The location for this provider can be configured in the plugin app Location Use degrees Fahrenheit and miles per hour Imperial units diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt index 188f183f..ae9cb8cc 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/LauncherSettingsData.kt @@ -393,6 +393,7 @@ data class LatLon( data class ProviderSettings( val locationId: String? = null, val locationName: String? = null, + val managedLocation: Boolean = false, ) @Serializable diff --git a/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt b/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt index 91b4ce18..f7bbc60a 100644 --- a/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt +++ b/core/preferences/src/main/java/de/mm20/launcher2/preferences/weather/WeatherSettings.kt @@ -20,6 +20,10 @@ sealed interface WeatherLocation { override val name: String, val locationId: String, ) : WeatherLocation + + data object Managed : WeatherLocation { + override val name: String = "Managed by plugin" + } } data class WeatherSettingsData( @@ -51,6 +55,10 @@ class WeatherSettings internal constructor( val location = launcherDataStore.data.map { val providerSettings = it.weatherProviderSettings[it.weatherProvider] + + if (providerSettings?.managedLocation == true) { + return@map WeatherLocation.Managed + } val id = providerSettings?.locationId val name = providerSettings?.locationName @@ -87,6 +95,7 @@ class WeatherSettings internal constructor( providerSettings.copy( locationId = null, locationName = null, + managedLocation = false, ) ) } @@ -104,7 +113,27 @@ class WeatherSettings internal constructor( it.weatherProvider, providerSettings.copy( locationId = location.locationId, - locationName = location.name + locationName = location.name, + managedLocation = false, + ) + ) + } + ) + } + + is WeatherLocation.Managed -> { + it.copy( + weatherLocation = null, + weatherLocationName = null, + weatherAutoLocation = true, + weatherLastUpdate = 0L, + weatherProviderSettings = it.weatherProviderSettings.toMutableMap().apply { + put( + it.weatherProvider, + providerSettings.copy( + locationId = null, + locationName = null, + managedLocation = true, ) ) } diff --git a/core/shared/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt b/core/shared/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt index 4f293cc2..70310068 100644 --- a/core/shared/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt +++ b/core/shared/src/main/java/de/mm20/launcher2/plugin/config/WeatherPluginConfig.kt @@ -1,7 +1,5 @@ package de.mm20.launcher2.plugin.config -import android.os.Bundle - data class WeatherPluginConfig( /** * Minimum time (in ms) that needs to pass before the provider can be queried again. @@ -9,4 +7,9 @@ data class WeatherPluginConfig( * or weather provider. */ val minUpdateInterval: Long = 60 * 60 * 1000L, + /** + * Whether the location is managed by the plugin. If true, the user cannot change the location + * in the launcher settings. + */ + val managedLocation: Boolean = false, ) \ No newline at end of file diff --git a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt index 5ea0d6a0..4d3ffe58 100644 --- a/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt +++ b/data/files/src/main/java/de/mm20/launcher2/files/providers/PluginFileProvider.kt @@ -10,6 +10,7 @@ import androidx.core.database.getIntOrNull import androidx.core.database.getLongOrNull import androidx.core.database.getStringOrNull import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.plugin.PluginApi import de.mm20.launcher2.plugin.config.SearchPluginConfig import de.mm20.launcher2.plugin.contracts.FilePluginContract import de.mm20.launcher2.plugin.contracts.PluginContract @@ -66,23 +67,7 @@ class PluginFileProvider( } private fun getPluginConfig(): SearchPluginConfig? { - val configBundle = try { - context.contentResolver.call( - Uri.Builder() - .scheme("content") - .authority(pluginAuthority) - .build(), - PluginContract.Methods.GetConfig, - null, - null - ) ?: return null - } catch (e: Exception) { - Log.e("MM20", "Plugin ${pluginAuthority} threw exception") - CrashReporter.logException(e) - return null - } - - return SearchPluginConfig(configBundle) + return PluginApi(pluginAuthority, context.contentResolver).getSearchPluginConfig() } suspend fun getFile(id: String): File? { diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherProviderInfo.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherProviderInfo.kt index ad8a2054..fed6a1dc 100644 --- a/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherProviderInfo.kt +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/WeatherProviderInfo.kt @@ -3,4 +3,5 @@ package de.mm20.launcher2.weather data class WeatherProviderInfo( val id: String, val name: String, + val managedLocation: Boolean = false, ) \ No newline at end of file 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 54cff8e2..5252a76b 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 @@ -8,6 +8,7 @@ import de.mm20.launcher2.devicepose.DevicePoseProvider import de.mm20.launcher2.ktx.or import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionsManager +import de.mm20.launcher2.plugin.PluginApi import de.mm20.launcher2.plugin.PluginRepository import de.mm20.launcher2.plugin.PluginType import de.mm20.launcher2.preferences.LatLon @@ -180,8 +181,9 @@ internal class WeatherRepositoryImpl( } val pluginProviders = pluginRepository.findMany(type = PluginType.Weather, enabled = true) return pluginProviders.map { - providers + it.map { - WeatherProviderInfo(it.authority, it.label) + providers + it.mapNotNull { + val config = PluginApi(it.authority, context.contentResolver).getWeatherPluginConfig() ?: return@mapNotNull null + WeatherProviderInfo(it.authority, it.label, config.managedLocation) } } } diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/plugin/PluginWeatherProvider.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/plugin/PluginWeatherProvider.kt index d2b3df6e..5592ad24 100644 --- a/data/weather/src/main/java/de/mm20/launcher2/weather/plugin/PluginWeatherProvider.kt +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/plugin/PluginWeatherProvider.kt @@ -10,6 +10,7 @@ import androidx.core.database.getIntOrNull import androidx.core.database.getLongOrNull import androidx.core.database.getStringOrNull import de.mm20.launcher2.crashreporter.CrashReporter +import de.mm20.launcher2.plugin.PluginApi import de.mm20.launcher2.plugin.config.WeatherPluginConfig import de.mm20.launcher2.plugin.contracts.PluginContract import de.mm20.launcher2.plugin.contracts.WeatherPluginContract @@ -27,11 +28,14 @@ internal class PluginWeatherProvider( private val pluginAuthority: String, ) : WeatherProvider { override suspend fun getWeatherData(location: WeatherLocation): List? { + val config = getPluginConfig() val uri = Uri.Builder() .scheme("content") .authority(pluginAuthority) .path(WeatherPluginContract.Paths.Forecasts).apply { - if (location is WeatherLocation.LatLon) { + if (config?.managedLocation == true || location is WeatherLocation.Managed) { + // no parameters + } else if (location is WeatherLocation.LatLon) { appendQueryParameter( WeatherPluginContract.ForecastParams.Lat, location.lat.toString() @@ -40,14 +44,15 @@ internal class PluginWeatherProvider( WeatherPluginContract.ForecastParams.Lon, location.lon.toString() ) + appendQueryParameter(WeatherPluginContract.ForecastParams.LocationName, location.name) } else if (location is WeatherLocation.Id) { appendQueryParameter( WeatherPluginContract.ForecastParams.Id, location.locationId ) + appendQueryParameter(WeatherPluginContract.ForecastParams.LocationName, location.name) } } - .appendQueryParameter(WeatherPluginContract.ForecastParams.LocationName, location.name) .appendQueryParameter(WeatherPluginContract.ForecastParams.Language, getLang()) .build() @@ -307,22 +312,6 @@ internal class PluginWeatherProvider( } private fun getPluginConfig(): WeatherPluginConfig? { - val configBundle = try { - context.contentResolver.call( - Uri.Builder() - .scheme("content") - .authority(pluginAuthority) - .build(), - PluginContract.Methods.GetConfig, - null, - null - ) ?: return null - } catch (e: Exception) { - Log.e("PluginWeatherProvider", "Plugin $pluginAuthority threw exception", e) - CrashReporter.logException(e) - return null - } - - return WeatherPluginConfig(configBundle) + return PluginApi(pluginAuthority, context.contentResolver).getWeatherPluginConfig() } } \ No newline at end of file diff --git a/docs/docs/developer-guide/plugins/plugin-types/common/_search_plugin_config.md b/docs/docs/developer-guide/plugins/plugin-types/common/_search_plugin_config.md index ce3827e0..35795b1a 100644 --- a/docs/docs/developer-guide/plugins/plugin-types/common/_search_plugin_config.md +++ b/docs/docs/developer-guide/plugins/plugin-types/common/_search_plugin_config.md @@ -1,5 +1,20 @@ Search plugins have the following configuration properties: -- `storageStrategy`: Describes how the launcher should store a search result in its internal database. This is relevant when a user pins a search result to favorites, or when they assign a tag or custom label. In these situations, the launcher needs to be able to restore the search result from its database. There are two different strategies: - - **`StorageStrategy.StoreReference`**: The launcher only stores the ID of the search result, and the plugin that created it. To restore a result, the plugin is queried again. This strategy allows the plugin provider to update a search result at a later point in time. However, plugins that use this strategy must guarantee that a search result can be restored in a timely manner. In particular, the plugin provider must be able to restore a search result without any network requests. - - **`StorageStrategy.StoreCopy`** (default): The launcher stores all relevant information about this search result in its own internal database. The result can be restored without querying the plugin again. This strategy is very easy to implement. The downside is, that results cannot be updated at a later point in time. +- `storageStrategy`: Describes how the launcher should store a search result in its internal + database. This is relevant when a user pins a search result to favorites, or when they assign a + tag or custom label. In these situations, the launcher needs to be able to restore the search + result from its database. There are two different strategies: + - **`StorageStrategy.StoreReference`**: The launcher only stores the ID of the search result, + and the plugin that created it. To restore a result, the plugin is queried again. This + strategy allows the plugin provider to update a search result at a later point in time. + However, plugins that use this strategy must guarantee that a search result can be restored in + a timely manner. In particular, the plugin provider must be able to restore a search result + without any network requests. + - **`StorageStrategy.StoreCopy`** (default): The launcher stores all relevant information about + this search result in its own internal database. The result can be restored without querying + the plugin again. This strategy is very easy to implement. The downside is, that results + cannot be updated at a later point in time. + - **`StorageStrategy.Deferred`** (default): The launcher stores all relevant information in its + own internal database, like [StoreCopy]. A fresh copy is fetched from the plugin provider when + the user opens the search result's detail view. This allows the plugin provider to update the + search result at a later point in time, without the time constraints of [StoreReference]. diff --git a/docs/docs/developer-guide/plugins/plugin-types/file-search.md b/docs/docs/developer-guide/plugins/plugin-types/file-search.md index dcc8bf65..02a4e558 100644 --- a/docs/docs/developer-guide/plugins/plugin-types/file-search.md +++ b/docs/docs/developer-guide/plugins/plugin-types/file-search.md @@ -1,6 +1,8 @@ # File Search -File search provider plugins need to extend the `FileProvider` class: +File search provider plugins need to extend +the `FileProvider` +class: ```kt class MyFileSearchPlugin : FileProvider( @@ -9,7 +11,9 @@ class MyFileSearchPlugin : FileProvider( ``` -In the super constructor call, pass a `SearchPluginConfig` object. +In the super constructor call, pass +a `SearchPluginConfig` +object. ## Plugin config @@ -24,7 +28,13 @@ suspend fun search(query: String, allowNetwork: Boolean): List ``` - `query` is the search term -- `allowNetwork` is a flag that indicates whether the user has enabled online search for this query. Plugins are generally advised to respect this request. This flag exists mainly for privacy reasons: the majority of searches target offline results (like apps, or contacts). Sending every single search request to external servers is overkill and can be a privacy issue. (Besides, it's not very nice to overload servers with unnecessary requests.) To reduce the amount of data that is leaked to external servers, users can control, whether a search should include online results or not. +- `allowNetwork` is a flag that indicates whether the user has enabled online search for this query. + Plugins are generally advised to respect this request. This flag exists mainly for privacy + reasons: the majority of searches target offline results (like apps, or contacts). Sending every + single search request to external servers is overkill and can be a privacy issue. (Besides, it's + not very nice to overload servers with unnecessary requests.) To reduce the amount of data that is + leaked to external servers, users can control, whether a search should include online results or + not. `search` returns a list of `File`s. The list can be empty if no results were found. @@ -32,26 +42,36 @@ suspend fun search(query: String, allowNetwork: Boolean): List A `File` has the following properties: -- `id`: A unique and stable identifier for this file. This is used to track usage stats so if two files are identical, they must have the same ID, and if they are different, they need to have different IDs. +- `id`: A unique and stable identifier for this file. This is used to track usage stats so if two + files are identical, they must have the same ID, and if they are different, they need to have + different IDs. - `uri`: A URI that is used to open the file. - `displayName`: The name that is shown to the user -- `mimeType`: The MIME type of the file. This is only used for informational purposes, i.e. to determine the icon. +- `mimeType`: The MIME type of the file. This is only used for informational purposes, i.e. to + determine the icon. - `size`: The file size in bytes. -- `path`: The file path. This is shown for informational purposes. It is not used to read or open the file. +- `path`: The file path. This is shown for informational purposes. It is not used to read or open + the file. - `isDirectory`: Whether the file is a folder. If true, a folder icon is shown. -- `thumbnailUri`: An optional URI to a file thumbnail. Supported schemes are: `content`, `file`, `android.resource`, `http`, and `https`. If this is a `content` URI, make sure that the launcher has the permissions to access it. -- `owner`: The name of the owner of the file. This is mainly relevant for files that are stored in a cloud drive and are not owned by the user themselves, but shared with them. +- `thumbnailUri`: An optional URI to a file thumbnail. Supported schemes + are: `content`, `file`, `android.resource`, `http`, and `https`. If this is a `content` URI, make + sure that the launcher has the permissions to access it. +- `owner`: The name of the owner of the file. This is mainly relevant for files that are stored in a + cloud drive and are not owned by the user themselves, but shared with them. - `metadata`: Additional file metadata. ## Get a file -If you have set `config.storageStrategy` to `StorageStrategy.StoreReference`, you must override +If you have set `config.storageStrategy` to `StorageStrategy.StoreReference` +or `StorageStrategy.Deferred`, you must override ```kt suspend fun get(id: String): File? ``` -This method is used to lookup a file by its `id`. If the file is no longer available, it should return `null`. In this case, the launcher will remove it from its database. +This method is used to lookup a file by its `id`. If the file is no longer available, it should +return `null`. In this case, the launcher will remove it from its database. If the file is +temporarily unavailable, an exception should be thrown. ## Plugin state diff --git a/docs/docs/developer-guide/plugins/plugin-types/weather.md b/docs/docs/developer-guide/plugins/plugin-types/weather.md index 5df459f5..25e21836 100644 --- a/docs/docs/developer-guide/plugins/plugin-types/weather.md +++ b/docs/docs/developer-guide/plugins/plugin-types/weather.md @@ -1,6 +1,8 @@ # Weather Provider Plugins -Weather provider plugins need to extend the `WeatherProvider` class: +Weather provider plugins need to extend +the `WeatherProvider` +class: ```kt class MyWeatherProviderPlugin : WeatherProvider( @@ -9,13 +11,20 @@ class MyWeatherProviderPlugin : WeatherProvider( ``` -In the super constructor call, pass a `WeatherPluginConfig` object. +In the super constructor call, pass +a `WeatherPluginConfig` +object. ## Plugin config In the plugin config, you can set the following properties: -- `minUpdateInterval`: Minimum time (in ms) that needs to pass before the provider can be queried again. The launcher respects this value as long as the user does not change the weather settings (provider or location). +- `minUpdateInterval`: Minimum time (in ms) that needs to pass before the provider can be queried + again. The launcher respects this value as long as the user does not change the weather settings ( + provider or location). + +- `managedLocation`: If true, the plugin will manage the location itself. This means that the user + cannot change the location settings in the launcher. ## Location search @@ -25,18 +34,26 @@ If your weather provider service provides an API to lookup locations, you should suspend fun findLocations(query: String, lang: String): List ``` -This method is called when a user has _Auto location_ disabled and they are trying to set a new location. +This method is called when a user has _Auto location_ disabled and they are trying to set a new +location. -The default implementation uses the Android Geocoder, but this API has the limitation that it relies on Google Play Services so you should use your own implementation whenever feasable. +The default implementation uses the Android Geocoder, but this API has the limitation that it relies +on Google Play Services so you should use your own implementation whenever feasable. -`findLocations` returns a list of `WeatherLocation`s. Return an empty list if no location has been found. +`findLocations` returns a list +of `WeatherLocation` +s. Return an empty list if no location has been found. ### Location types -There are two types of locations: +There are three types of locations: -- `WeatherLocation.LatLon`: use this if your weather service identifies locations by their geo coordinates. -- `WeatherLocation.Id`: use this if your weather service has an internal ID system to identify locations. +- `WeatherLocation.LatLon`: + use this if your weather service identifies locations by their geo coordinates. +- `WeatherLocation.Id`: + use this if your weather service has an internal ID system to identify locations. +- `WeatherLocation.Managed`: + a special location that indicates that the plugin should determine the location itself. ## Featch weather data @@ -50,16 +67,30 @@ suspend fun getWeatherData(lat: Double, lon: Double, lang: String?): List? ``` -The first method is called when the user has _Auto location_ enabled. `lat` and `lon` the last known coordinates of the user. +The first method is called when the user has _Auto location_ enabled. `lat` and `lon` the last known +coordinates of the user. -The second method is called when the user has set their location to a fixed location. `location` is guaranteed to be a value that has been returned by `findLocations` before. If you haven't overriden `findLocations`, this will always be a `WeatherLocation.LatLon`. +The second method is called when the user has set their location to a fixed location. In most cases, +`location` will be a value that has been returned by `findLocations` before, but it's possible that +`location` is a `WeatherLocation.LatLon` if the user has changed the provider after setting a fixed +location. If you haven't overridden `findLocations`, this will always be a `WeatherLocation.LatLon`. +If `managedLocation` is set to `true`, this method is called with `WeatherLocation.Managed`. -Both methods return a list of `Forecast`s. If an error occurs, you can throw an exception or return `null`, in this case the launcher will keep the old data and start another attempt at a later time. +Both methods return a list +of `Forecast` +s. If an error occurs, you can throw an exception or return `null`, in this case the launcher will +keep the old data and start another attempt at a later time. -`Forecast` objects need at least a `timestamp` (unix time in millis), a `temperature`, a `condition`, an `icon`, a `location` name, and a `provider` name. +`Forecast` objects need at least a `timestamp` (unix time in millis), a `temperature`, +a `condition`, an `icon`, a `location` name, and a `provider` name. -- The `condition` should preferably be localized in the user's language, which is provided by the `lang` parameter. -- To construct a `Temperature`, you can use the `Double.C`, `Double.F`, or `Double.K` helper functions, depending on whether the numeric value returned by your weather service API is in degrees celsius, degrees fahrenheit, or kelvin: +- The `condition` should preferably be localized in the user's language, which is provided by + the `lang` parameter. +- To construct + a `Temperature`, + you can use the `Double.C`, `Double.F`, or `Double.K` helper functions, depending on whether the + numeric value returned by your weather service API is in degrees celsius, degrees fahrenheit, or + kelvin: ```kt val temp = tempValueInCelcius.C @@ -69,12 +100,15 @@ Both methods return a list of `getLocationName` method to reverse geocode the location name using Android's Geocoder API. + - In fixed location mode, you should read this value from the `location` parameter, to ensure + that the name in the weather widget matches the name that the user has set in preferences. + - In auto location mode, if your weather service does not give you a location name, you can use + the `getLocationName` + method to reverse geocode the location name using Android's Geocoder API. ## Plugin state diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fb5e0fb..aac884f2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ minSdk = "26" compileSdk = "34" targetSdk = "34" -pluginSdk = "1.0.0" +pluginSdk = "1.1.0" gradle = "8.1.2" android-gradle-plugin = "8.2.2" diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/config/WeatherPluginConfig.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/config/WeatherPluginConfig.kt index f2c24621..7722795a 100644 --- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/config/WeatherPluginConfig.kt +++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/config/WeatherPluginConfig.kt @@ -6,5 +6,6 @@ import de.mm20.launcher2.plugin.config.WeatherPluginConfig internal fun WeatherPluginConfig.toBundle(): Bundle { return Bundle().apply { putLong("minUpdateInterval", minUpdateInterval) + putBoolean("managedLocation", managedLocation) } } \ No newline at end of file diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherLocation.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherLocation.kt index 445d8ed5..6675055d 100644 --- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherLocation.kt +++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherLocation.kt @@ -7,4 +7,8 @@ sealed interface WeatherLocation { data class LatLon(override val name: String, val lat: Double, val lon: Double) : WeatherLocation + + data object Managed: WeatherLocation { + override val name: String = "" + } } diff --git a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherProvider.kt b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherProvider.kt index 908ce63a..97524419 100644 --- a/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherProvider.kt +++ b/plugins/sdk/src/main/java/de/mm20/launcher2/sdk/weather/WeatherProvider.kt @@ -107,6 +107,9 @@ abstract class WeatherProvider( if (locationName != null && lat != null && lon != null) { return getWeatherData(WeatherLocation.LatLon(locationName, lat, lon), lang) } + if (lat == null && lon == null && id == null) { + return getWeatherData(WeatherLocation.Managed, lang) + } return null } @@ -226,6 +229,9 @@ abstract class WeatherProvider( */ open suspend fun findLocations(query: String, lang: String): List { val context = context ?: return emptyList() + if (config.managedLocation) { + return listOf(WeatherLocation.Managed) + } val parts = query.split(" ", limit = 3) val lat = parts.getOrNull(0)?.toDoubleOrNull() val lon = parts.getOrNull(1)?.toDoubleOrNull()