Add managed location to weather plugin API
This commit is contained in:
parent
b0f9ffd473
commit
1d6688831c
@ -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) {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -8,5 +8,6 @@ fun WeatherPluginConfig(bundle: Bundle): WeatherPluginConfig {
|
||||
"minUpdateInterval",
|
||||
60 * 60 * 1000L
|
||||
),
|
||||
managedLocation = bundle.getBoolean("managedLocation", false)
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -393,6 +393,7 @@ data class LatLon(
|
||||
data class ProviderSettings(
|
||||
val locationId: String? = null,
|
||||
val locationName: String? = null,
|
||||
val managedLocation: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
@ -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? {
|
||||
|
||||
@ -3,4 +3,5 @@ package de.mm20.launcher2.weather
|
||||
data class WeatherProviderInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val managedLocation: Boolean = false,
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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].
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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 = ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user