Implement plugin weather provider
This commit is contained in:
parent
d80e4c66a4
commit
211f30170c
@ -1,5 +1,7 @@
|
||||
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.
|
||||
@ -7,4 +9,22 @@ data class WeatherPluginConfig(
|
||||
* or weather provider.
|
||||
*/
|
||||
val minUpdateInterval: Long = 60 * 60 * 1000L,
|
||||
) {
|
||||
|
||||
fun toBundle(): Bundle {
|
||||
return Bundle().apply {
|
||||
putLong("minUpdateInterval", minUpdateInterval)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(bundle: Bundle): WeatherPluginConfig {
|
||||
return WeatherPluginConfig(
|
||||
minUpdateInterval = bundle.getLong(
|
||||
"minUpdateInterval",
|
||||
60 * 60 * 1000L
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
object WeatherPluginContract {
|
||||
object Paths {
|
||||
const val Weather = "weather"
|
||||
const val Forecasts = "forecasts"
|
||||
const val Locations = "locations"
|
||||
}
|
||||
|
||||
|
||||
@ -5,13 +5,14 @@ import de.mm20.launcher2.weather.brightsky.BrightSkyProvider
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
|
||||
import de.mm20.launcher2.weather.plugin.PluginWeatherProvider
|
||||
import de.mm20.launcher2.weather.settings.WeatherSettings
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val weatherModule = module {
|
||||
single<WeatherRepository> { WeatherRepositoryImpl(androidContext(), get(), get()) }
|
||||
single<WeatherRepository> { WeatherRepositoryImpl(androidContext(), get(), get(), get()) }
|
||||
single<WeatherSettings> { WeatherSettings(androidContext()) }
|
||||
factory<Backupable>(named<WeatherSettings>()) { get<WeatherSettings>() }
|
||||
factory<WeatherProvider> { (providerId: String) ->
|
||||
@ -20,7 +21,7 @@ val weatherModule = module {
|
||||
MetNoProvider.Id -> MetNoProvider(androidContext(), get())
|
||||
HereProvider.Id -> HereProvider(androidContext())
|
||||
BrightSkyProvider.Id -> BrightSkyProvider(androidContext())
|
||||
else -> TODO("Implement plugin provider")
|
||||
else -> PluginWeatherProvider(androidContext(), providerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,9 @@ import org.koin.core.parameter.parametersOf
|
||||
|
||||
interface WeatherProvider {
|
||||
|
||||
val updateInterval: Long
|
||||
get() = 1000 * 60 * 60L
|
||||
suspend fun getUpdateInterval(): Long {
|
||||
return 1000 * 60 * 60L
|
||||
}
|
||||
|
||||
suspend fun getWeatherData(location: WeatherLocation): List<Forecast>?
|
||||
suspend fun getWeatherData(lat: Double, lon: Double): List<Forecast>?
|
||||
|
||||
@ -11,6 +11,8 @@ import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
import de.mm20.launcher2.plugin.PluginType
|
||||
import de.mm20.launcher2.weather.brightsky.BrightSkyProvider
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
@ -39,6 +41,7 @@ internal class WeatherRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val database: AppDatabase,
|
||||
private val settings: WeatherSettings,
|
||||
private val pluginRepository: PluginRepository,
|
||||
) : WeatherRepository, KoinComponent {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
@ -158,7 +161,12 @@ internal class WeatherRepositoryImpl(
|
||||
if (HereProvider.isAvailable(context)) {
|
||||
providers.add(WeatherProviderInfo(HereProvider.Id, context.getString(R.string.provider_here)))
|
||||
}
|
||||
return flowOf(providers)
|
||||
val pluginProviders = pluginRepository.findMany(type = PluginType.Weather, enabled = true)
|
||||
return pluginProviders.map {
|
||||
providers + it.map {
|
||||
WeatherProviderInfo(it.authority, it.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,7 +181,7 @@ class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
|
||||
val settingsData = settings.data.first()
|
||||
val provider = WeatherProvider.getInstance(settingsData.provider)
|
||||
|
||||
val updateInterval = provider.updateInterval
|
||||
val updateInterval = provider.getUpdateInterval()
|
||||
val lastUpdate = settingsData.lastUpdate
|
||||
|
||||
if (lastUpdate + updateInterval > System.currentTimeMillis()) {
|
||||
|
||||
@ -0,0 +1,327 @@
|
||||
package de.mm20.launcher2.weather.plugin
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.CancellationSignal
|
||||
import android.util.Log
|
||||
import androidx.core.database.getDoubleOrNull
|
||||
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.config.WeatherPluginConfig
|
||||
import de.mm20.launcher2.plugin.contracts.PluginContract
|
||||
import de.mm20.launcher2.plugin.contracts.WeatherPluginContract
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal class PluginWeatherProvider(
|
||||
private val context: Context,
|
||||
private val pluginAuthority: String,
|
||||
) : WeatherProvider {
|
||||
override suspend fun getWeatherData(location: WeatherLocation): List<Forecast>? {
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(pluginAuthority)
|
||||
.path(WeatherPluginContract.Paths.Forecasts).apply {
|
||||
if (location is WeatherLocation.LatLon) {
|
||||
appendQueryParameter(
|
||||
WeatherPluginContract.ForecastParams.Lat,
|
||||
location.lat.toString()
|
||||
)
|
||||
appendQueryParameter(
|
||||
WeatherPluginContract.ForecastParams.Lon,
|
||||
location.lon.toString()
|
||||
)
|
||||
} else if (location is WeatherLocation.Id) {
|
||||
appendQueryParameter(
|
||||
WeatherPluginContract.ForecastParams.Id,
|
||||
location.locationId
|
||||
)
|
||||
}
|
||||
}
|
||||
.appendQueryParameter(WeatherPluginContract.ForecastParams.LocationName, location.name)
|
||||
.appendQueryParameter(WeatherPluginContract.ForecastParams.Language, getLang())
|
||||
.build()
|
||||
|
||||
return getWeatherData(uri)
|
||||
}
|
||||
|
||||
override suspend fun getWeatherData(lat: Double, lon: Double): List<Forecast>? {
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(pluginAuthority)
|
||||
.appendQueryParameter(WeatherPluginContract.ForecastParams.Lat, lat.toString())
|
||||
.appendQueryParameter(WeatherPluginContract.ForecastParams.Lon, lon.toString())
|
||||
.appendQueryParameter(WeatherPluginContract.ForecastParams.Language, getLang())
|
||||
.build()
|
||||
|
||||
return getWeatherData(uri)
|
||||
}
|
||||
|
||||
private suspend fun getWeatherData(uri: Uri): List<Forecast>? = withContext(Dispatchers.IO) {
|
||||
val cancellationSignal = CancellationSignal()
|
||||
return@withContext suspendCancellableCoroutine {
|
||||
it.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
}
|
||||
val cursor = try {
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
cancellationSignal
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Plugin $pluginAuthority threw exception")
|
||||
CrashReporter.logException(e)
|
||||
it.resume(emptyList())
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
if (cursor == null) {
|
||||
Log.e("MM20", "Plugin $pluginAuthority returned null cursor")
|
||||
it.resume(emptyList())
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
val results = forecastsFromCursor(cursor) ?: emptyList()
|
||||
it.resume(results)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forecastsFromCursor(cursor: Cursor): List<Forecast>? {
|
||||
return cursor.use {
|
||||
val results = mutableListOf<Forecast>()
|
||||
|
||||
val timestampIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Timestamp)
|
||||
.takeIf { it >= 0 } ?: return null
|
||||
val createdAtIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.CreatedAt)
|
||||
.takeIf { it >= 0 } ?: return null
|
||||
val temperatureIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Temperature)
|
||||
.takeIf { it >= 0 } ?: return null
|
||||
val conditionIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Condition)
|
||||
.takeIf { it >= 0 } ?: return null
|
||||
val iconIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Icon).takeIf { it >= 0 }
|
||||
?: return null
|
||||
|
||||
val locationIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Location)
|
||||
.takeIf { it >= 0 }
|
||||
?: return null
|
||||
|
||||
val providerIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Provider)
|
||||
.takeIf { it >= 0 }
|
||||
?: return null
|
||||
|
||||
val providerUrlIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.ProviderUrl)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val precipitationIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Precipitation)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val precipProbabilityIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.RainProbability)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val cloudsIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Clouds)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val humidityIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Humidity)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val windSpeedIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.WindSpeed)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val windDirectionIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.WindDirection)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val pressureIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Pressure)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val nightIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.Night)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val minTempIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.TemperatureMin)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
val maxTempIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.ForecastColumns.TemperatureMax)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
results += Forecast(
|
||||
timestamp = cursor.getLongOrNull(timestampIndex) ?: continue,
|
||||
temperature = cursor.getDoubleOrNull(temperatureIndex) ?: continue,
|
||||
updateTime = cursor.getLongOrNull(createdAtIndex) ?: continue,
|
||||
condition = cursor.getStringOrNull(conditionIndex) ?: continue,
|
||||
icon = getIcon(cursor.getStringOrNull(iconIndex) ?: continue),
|
||||
location = cursor.getStringOrNull(locationIndex) ?: continue,
|
||||
provider = cursor.getStringOrNull(providerIndex) ?: continue,
|
||||
providerUrl = providerUrlIndex?.let { cursor.getStringOrNull(it) } ?: "",
|
||||
clouds = cloudsIndex?.let { cursor.getIntOrNull(it) },
|
||||
humidity = humidityIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
precipitation = precipitationIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
precipProbability = precipProbabilityIndex?.let { cursor.getIntOrNull(it) },
|
||||
windSpeed = windSpeedIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
windDirection = windDirectionIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
pressure = pressureIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
night = nightIndex?.let { cursor.getIntOrNull(it) } == 1,
|
||||
minTemp = minTempIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
maxTemp = maxTempIndex?.let { cursor.getDoubleOrNull(it) },
|
||||
)
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIcon(icon: String): Int {
|
||||
return when (icon) {
|
||||
"Clear" -> Forecast.CLEAR
|
||||
"Cloudy" -> Forecast.CLOUDY
|
||||
"Cold" -> Forecast.COLD
|
||||
"Drizzle" -> Forecast.DRIZZLE
|
||||
"Haze" -> Forecast.HAZE
|
||||
"Fog" -> Forecast.FOG
|
||||
"Hail" -> Forecast.HAIL
|
||||
"HeavyThunderstorm" -> Forecast.HEAVY_THUNDERSTORM
|
||||
"HeavyThunderstormWithRain" -> Forecast.HEAVY_THUNDERSTORM_WITH_RAIN
|
||||
"Hot" -> Forecast.HOT
|
||||
"MostlyCloudy" -> Forecast.MOSTLY_CLOUDY
|
||||
"PartlyCloudy" -> Forecast.PARTLY_CLOUDY
|
||||
"Showers" -> Forecast.SHOWERS
|
||||
"Sleet" -> Forecast.SLEET
|
||||
"Snow" -> Forecast.SNOW
|
||||
"Storm" -> Forecast.STORM
|
||||
"Thunderstorm" -> Forecast.THUNDERSTORM
|
||||
"ThunderstormWithRain" -> Forecast.THUNDERSTORM_WITH_RAIN
|
||||
"Wind" -> Forecast.WIND
|
||||
"BrokenClouds" -> Forecast.BROKEN_CLOUDS
|
||||
else -> Forecast.NONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLang(): String {
|
||||
return Locale.getDefault().language
|
||||
}
|
||||
|
||||
override suspend fun findLocation(query: String): List<WeatherLocation> = withContext(Dispatchers.IO) {
|
||||
val cancellationSignal = CancellationSignal()
|
||||
return@withContext suspendCancellableCoroutine {
|
||||
it.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
}
|
||||
val uri = Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(pluginAuthority)
|
||||
.path(WeatherPluginContract.Paths.Locations)
|
||||
.appendQueryParameter(WeatherPluginContract.LocationParams.Query, query)
|
||||
.appendQueryParameter(WeatherPluginContract.LocationParams.Language, getLang())
|
||||
.build()
|
||||
|
||||
val cursor = try {
|
||||
context.contentResolver.query(
|
||||
uri,
|
||||
null,
|
||||
null,
|
||||
cancellationSignal
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Plugin $pluginAuthority threw exception")
|
||||
CrashReporter.logException(e)
|
||||
it.resume(emptyList())
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
if (cursor == null) {
|
||||
Log.e("MM20", "Plugin $pluginAuthority returned null cursor")
|
||||
it.resume(emptyList())
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
|
||||
val results = locationsFromCursor(cursor) ?: emptyList()
|
||||
it.resume(results)
|
||||
}
|
||||
}
|
||||
|
||||
private fun locationsFromCursor(cursor: Cursor): List<WeatherLocation> {
|
||||
return cursor.use {
|
||||
val results = mutableListOf<WeatherLocation>()
|
||||
|
||||
val nameIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.LocationColumns.Name)
|
||||
.takeIf { it >= 0 } ?: return emptyList()
|
||||
val latIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.LocationColumns.Lat)
|
||||
.takeIf { it >= 0 }
|
||||
val lonIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.LocationColumns.Lon)
|
||||
.takeIf { it >= 0 }
|
||||
val locationIdIndex =
|
||||
cursor.getColumnIndex(WeatherPluginContract.LocationColumns.Id)
|
||||
.takeIf { it >= 0 }
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val lat = latIndex?.let { cursor.getDoubleOrNull(it) }
|
||||
val lon = lonIndex?.let { cursor.getDoubleOrNull(it) }
|
||||
val locationId = locationIdIndex?.let { cursor.getStringOrNull(it) }
|
||||
val name = cursor.getStringOrNull(nameIndex) ?: continue
|
||||
|
||||
if (lat != null && lon != null) {
|
||||
results += WeatherLocation.LatLon(lat = lat, lon = lon, name = name)
|
||||
} else if (locationId != null) {
|
||||
results += WeatherLocation.Id(locationId, name)
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUpdateInterval(): Long {
|
||||
return getPluginConfig()?.minUpdateInterval ?: super.getUpdateInterval()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,7 @@ abstract class WeatherProvider(
|
||||
checkPermissionOrThrow(context)
|
||||
|
||||
when {
|
||||
uri.pathSegments.size == 1 && uri.pathSegments.first() == WeatherPluginContract.Paths.Weather -> {
|
||||
uri.pathSegments.size == 1 && uri.pathSegments.first() == WeatherPluginContract.Paths.Forecasts -> {
|
||||
val lat = uri.getQueryParameter(WeatherPluginContract.ForecastParams.Lat)
|
||||
?.toDoubleOrNull()
|
||||
val lon = uri.getQueryParameter(WeatherPluginContract.ForecastParams.Lon)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user