Add managed location to weather plugin API

This commit is contained in:
MM20 2024-05-03 13:02:17 +02:00
parent b0f9ffd473
commit 1d6688831c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
19 changed files with 256 additions and 100 deletions

View File

@ -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) {

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -8,5 +8,6 @@ fun WeatherPluginConfig(bundle: Bundle): WeatherPluginConfig {
"minUpdateInterval",
60 * 60 * 1000L
),
managedLocation = bundle.getBoolean("managedLocation", false)
)
}

View File

@ -481,6 +481,8 @@
<string name="preference_category_location">Location</string>
<string name="preference_automatic_location">Automatic location</string>
<string name="preference_automatic_location_summary">Use GPS and location services to determine location automatically</string>
<string name="preference_location_managed">Managed by plugin</string>
<string name="preference_location_managed_summary">The location for this provider can be configured in the plugin app</string>
<string name="preference_location">Location</string>
<string name="preference_imperial_units_summary">Use degrees Fahrenheit and miles per hour</string>
<string name="preference_imperial_units">Imperial units</string>

View File

@ -393,6 +393,7 @@ data class LatLon(
data class ProviderSettings(
val locationId: String? = null,
val locationName: String? = null,
val managedLocation: Boolean = false,
)
@Serializable

View File

@ -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,
)
)
}

View File

@ -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,
)

View File

@ -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? {

View File

@ -3,4 +3,5 @@ package de.mm20.launcher2.weather
data class WeatherProviderInfo(
val id: String,
val name: String,
val managedLocation: Boolean = false,
)

View File

@ -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)
}
}
}

View File

@ -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<Forecast>? {
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()
}
}

View File

@ -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].

View File

@ -1,6 +1,8 @@
# File Search
File search provider plugins need to extend the <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.files/-file-provider/index.html" target="_blank">`FileProvider`</a> class:
File search provider plugins need to extend
the <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.files/-file-provider/index.html" target="_blank">`FileProvider`</a>
class:
```kt
class MyFileSearchPlugin : FileProvider(
@ -9,7 +11,9 @@ class MyFileSearchPlugin : FileProvider(
```
In the super constructor call, pass a <a href="/reference/core/shared/de.mm20.launcher2.plugin.config/-search-plugin-config/index.html" target="_blank">`SearchPluginConfig`</a> object.
In the super constructor call, pass
a <a href="/reference/core/shared/de.mm20.launcher2.plugin.config/-search-plugin-config/index.html" target="_blank">`SearchPluginConfig`</a>
object.
## Plugin config
@ -24,7 +28,13 @@ suspend fun search(query: String, allowNetwork: Boolean): List<File>
```
- `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<File>
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

View File

@ -1,6 +1,8 @@
# Weather Provider Plugins
Weather provider plugins need to extend the <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-provider/index.html" target="_blank">`WeatherProvider`</a> class:
Weather provider plugins need to extend
the <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-provider/index.html" target="_blank">`WeatherProvider`</a>
class:
```kt
class MyWeatherProviderPlugin : WeatherProvider(
@ -9,13 +11,20 @@ class MyWeatherProviderPlugin : WeatherProvider(
```
In the super constructor call, pass a <a href="/reference/core/shared/de.mm20.launcher2.plugin.config/-weather-plugin-config/index.html" target="_blank">`WeatherPluginConfig`</a> object.
In the super constructor call, pass
a <a href="/reference/core/shared/de.mm20.launcher2.plugin.config/-weather-plugin-config/index.html" target="_blank">`WeatherPluginConfig`</a>
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<WeatherLocation>
```
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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/index.html" target="_blank">`WeatherLocation`</a>s. Return an empty list if no location has been found.
`findLocations` returns a list
of <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/index.html" target="_blank">`WeatherLocation`</a>
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:
- <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/-lat-lon/index.html" target="_blank">`WeatherLocation.LatLon`</a>: use this if your weather service identifies locations by their geo coordinates.
- <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/-id/index.html" target="_blank">`WeatherLocation.Id`</a>: use this if your weather service has an internal ID system to identify locations.
- <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/-lat-lon/index.html" target="_blank">`WeatherLocation.LatLon`</a>:
use this if your weather service identifies locations by their geo coordinates.
- <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/-id/index.html" target="_blank">`WeatherLocation.Id`</a>:
use this if your weather service has an internal ID system to identify locations.
- <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-location/-managed/index.html" target="_blank">`WeatherLocation.Managed`</a>:
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<Foreca
suspend fun getWeatherData(location: WeatherLocation, lang: String?): List<Forecast>?
```
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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-forecast/index.html" target="_blank">`Forecast`</a>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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-forecast/index.html" target="_blank">`Forecast`</a>
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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-temperature/index.html" target="_blank">`Temperature`</a>, 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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-temperature/index.html" target="_blank">`Temperature`</a>,
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 <a href="/reference/plugins/sdk/de.mm20.launcher2.
Similar helper functions are available to construct
- `Pressure` (`Double.hPa`, and `Double.mbar`), and
- `WindSpeed` values (`Double.m_s`, `Double.km_h`, and `Double.mph`)
- `Pressure` (`Double.hPa`, and `Double.mbar`), and
- `WindSpeed` values (`Double.m_s`, `Double.km_h`, and `Double.mph`)
- `location` is the name of the location.
- 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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-provider/get-location-name.html" target="_blank">`getLocationName`</a> 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 <a href="/reference/plugins/sdk/de.mm20.launcher2.sdk.weather/-weather-provider/get-location-name.html" target="_blank">`getLocationName`</a>
method to reverse geocode the location name using Android's Geocoder API.
## Plugin state

View File

@ -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"

View File

@ -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)
}
}

View File

@ -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 = ""
}
}

View File

@ -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<WeatherLocation> {
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()