Add support for location plugins (#772)

* Cherry pick location refactor

* Refactor :data:openstreetmaps to :data:locations

* contract, plugin sdk

* Implement serialization, module tweaks

* Include check for out-of-date departures in SearchableItemVM.requestUpdatedSearchable()

* settings for location plugins

* Try not to be too lazy

* more fiddling with the plugin SDK

* add departures in MapView with mock data for debug builds

* change icons

* add boats

* animate departure lazycolumn

* Add MarqueeText for text overflow handling

* Define height for Departures in case there is no map to display

* Don't inclure railway / highway tags for OSM since there will be location plugins for that

* sort by time

* - Apply pre-merge changes to LocationSettings
- Add banner warning about slowed down location search for large search radii

* ditch `showLocationOnMap`

* LocationItem: make `showOpeningSchedule` toggleable

* LocationItem: make Navigation AssistChip work for people that don't have google maps installed

* LocationItem: resolve TODOs

* MapTiles: ditch unused code, animate userIndicator

* Reintroduce departure list

* Add LineColor

* Add osm tag `stars` as `userRating` https://taginfo.openstreetmap.org/keys/stars#overview

* typealias -> import

* Don't add Navigation Chip when there is no way to resolve navigation intents

* Add settings migration

* Set plugin SDK version to 1.2.0-SNAPSHOT

* Deduplicate shared plugin classes, use kotlinx.serialization

* Fix imports

* Use ZonedDateTime for depature times

* Add more line types

* Rewrite location serialization

* Replace street/houseNumber with address

* Add attribution field

* Add plugin config

* Reject location search requests without lat lon parameters

* Add default values to plugin location class

* Don't crash if column value is null

* Add docs comments to LocationCategory values

* Refactor OpeningSchedule as polymorphic

* remove dead corpse *ahem* code

* Split LocationCategory into category and icon

(Also update to Kotlin 2.0, please don't do this at home)

* Add more location icons

* Fix (?) location deserializer

* Add more location icons

* More icons

* Meh

* Add Pub

* Disable Github Maven repo if credentials are missing

* Add location search specific settings to plugin details screen

* Add language parameter

* Unbreak the build

* Refactor plugin SDK (with breaking changes)

* Set plugin SDK version to 2.0.0-SNAPSHOT

* Document SDK breaking changes

* Implement LocationProvider.getQuery

* Add a typesafe cursor API

* Oops I did it again

next time maybe check if the code is actually compiling before pushing

* Add missing return statement

* Fix list serialization

* Departure time UI adjustment

* Use typesafe cursor for weather plugins

* Add userRatingCount and emailAddress fields

* grrr

* Rename and extend LineTypes

* Add default lineType to Departure to fix serialization errors

* Fix refreshing stored plugin locations

* Adapt line name column width to available departures

* Fix plugin settings screen category overlap

* add LocationItem.GenericTransit

* Fix crash during deserialization of locations

* Update SDK docs

* Replace plugin "official" mark with "verified developer" mark

before anyone gets sued

* show 'now' when departure is in less than one minute

* Add typesafe Bundle API

* Implement plugin API changes

* Plugin SDK: Fix refresh result not being returned

* Update docs

* apply alpha to departures that have departured

* better (maybe): reduce saturation instead of alpha

* Add default values for Attribution

* Display attribution

* Rearrange location result layout

* Reduce searchable update interval to 1 minute

* Pass last update time to refresh function

* Change refresh path and ensure that timestamp is only update when the item was updated

* categorize osm location

* Update docs

* Optimize location search

- run providers in parallel
- flatten code

* add experimental address parsing for OSM

* add poi_category_townhall

* Fix popup closing when favorites items are updated

* Revert "Fix popup closing when favorites items are updated"

This reverts commit fc517fd066c7f8109b6d6df2d4f536af66398207.

* Fork AndroidAddressFormatter to `:libs:address-formatter`

* migrate `:libs:address-formatter` dependencies to version catalog and update them

* also consider addr:{suburb,hamlet} for `Address.city` if city tag is missing

* Move poi strings back to strings.xml

* Update Jetpack Compose

* Move address-formatter back to its original package, add license and readme

* Move address-formatter back to its original package

---------

Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
Christoph
2024-06-14 11:57:03 +02:00
committed by GitHub
co-authored by MM20
parent cfe80ff3e5
commit 65a9c8c1fe
145 changed files with 5804 additions and 2199 deletions
@@ -0,0 +1,195 @@
package de.mm20.launcher2.sdk.base
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.os.CancellationSignal
import de.mm20.launcher2.plugin.config.QueryPluginConfig
import de.mm20.launcher2.plugin.contracts.SearchPluginContract
import de.mm20.launcher2.sdk.config.toBundle
import de.mm20.launcher2.sdk.utils.launchWithCancellationSignal
data class SearchParams(
val allowNetwork: Boolean,
val lang: String?,
)
data class GetParams(
val lang: String?,
)
/**
* Parameters that are passed to the [refresh] method.
*/
data class RefreshParams(
/**
* The current language of the launcher.
*/
val lang: String?,
/**
* The time (in unixtime millis) when the item was last refreshed.
*/
val lastUpdated: Long,
)
abstract class QueryPluginProvider<TQuery, TResult>(
private val config: QueryPluginConfig,
) : BasePluginProvider() {
abstract suspend fun search(query: TQuery, params: SearchParams): List<TResult>
/**
* Get an item by its id.
* This only needs to be implemented if `config.storageStrategy` is set to `StoreReference`
*/
open suspend fun get(id: String, params: GetParams): TResult? = null
/**
* Request an updated copy of the item.
* This is called when `config.storageStrategy` is set to `StoreCopy` and the launcher wants to refresh the item.
* By default, this method returns the same item.
* @param item the old item that should be refreshed
* @param params the parameters that should be used to refresh the item
*/
open suspend fun refresh(item: TResult, params: RefreshParams): TResult? = item
internal abstract fun getQuery(uri: Uri): TQuery?
override fun onCreate(): Boolean = true
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? = query(uri, projection, null, null)
override fun query(
uri: Uri,
projection: Array<out String>?,
queryArgs: Bundle?,
cancellationSignal: CancellationSignal?
): Cursor? {
val context = context ?: return null
checkPermissionOrThrow(context)
when {
uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Search -> {
val query = getQuery(uri) ?: return null
val params = getSearchParams(uri)
val results = search(query, params, cancellationSignal)
return results.toCursor()
}
uri.pathSegments.size == 2 && uri.pathSegments.first() == SearchPluginContract.Paths.Root -> {
val id = uri.pathSegments[1]
val params = getGetParams(uri)
val result = get(id, params, cancellationSignal)
return if (result != null) {
listOf(result).toCursor()
} else {
emptyList<TResult>().toCursor()
}
}
uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Refresh -> {
val oldItem = queryArgs?.toResult() ?: return null
val params = getRefreshParams(uri)
val newItem = refresh(oldItem, params, cancellationSignal)
return if (newItem == null) {
emptyList<TResult>().toCursor()
} else {
listOf(newItem).toCursor().apply {
extras = Bundle().apply {
putBoolean(SearchPluginContract.Extras.NotUpdated, newItem === oldItem)
}
}
}
}
}
return null
}
override fun getType(uri: Uri): String? =
throw UnsupportedOperationException("This operation is not supported")
override fun insert(uri: Uri, values: ContentValues?): Uri? =
throw UnsupportedOperationException("This operation is not supported")
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int =
throw UnsupportedOperationException("This operation is not supported")
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int = throw UnsupportedOperationException("This operation is not supported")
private fun search(
query: TQuery,
params: SearchParams,
cancellationSignal: CancellationSignal?
): List<TResult> {
return launchWithCancellationSignal(cancellationSignal) {
search(query, params)
}
}
private fun refresh(
item: TResult,
params: RefreshParams,
cancellationSignal: CancellationSignal?
): TResult? {
return launchWithCancellationSignal(cancellationSignal) {
refresh(item, params)
}
}
private fun get(
id: String,
params: GetParams,
cancellationSignal: CancellationSignal?
): TResult? {
return launchWithCancellationSignal(cancellationSignal) {
get(id, params)
}
}
private fun getGetParams(uri: Uri): GetParams {
val lang = uri.getQueryParameter(SearchPluginContract.Params.Lang)
return GetParams(
lang = lang,
)
}
private fun getSearchParams(uri: Uri): SearchParams {
val allowNetwork =
uri.getQueryParameter(SearchPluginContract.Params.AllowNetwork)?.toBoolean()
?: false
val lang = uri.getQueryParameter(SearchPluginContract.Params.Lang)
return SearchParams(
allowNetwork = allowNetwork,
lang = lang,
)
}
private fun getRefreshParams(uri: Uri): RefreshParams {
val lang = uri.getQueryParameter(SearchPluginContract.Params.Lang)
val lastUpdated =
uri.getQueryParameter(SearchPluginContract.Params.UpdatedAt)?.toLongOrNull() ?: 0L
return RefreshParams(
lang = lang,
lastUpdated = lastUpdated,
)
}
internal abstract fun List<TResult>.toCursor(): Cursor
internal abstract fun Bundle.toResult(): TResult?
final override fun getPluginConfig(): Bundle {
return config.toBundle()
}
}
@@ -1,123 +0,0 @@
package de.mm20.launcher2.sdk.base
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.Bundle
import android.os.CancellationSignal
import de.mm20.launcher2.plugin.config.SearchPluginConfig
import de.mm20.launcher2.plugin.contracts.SearchPluginContract
import de.mm20.launcher2.sdk.config.toBundle
import de.mm20.launcher2.sdk.utils.launchWithCancellationSignal
import kotlinx.coroutines.runBlocking
abstract class SearchPluginProvider<T>(
private val config: SearchPluginConfig,
) : BasePluginProvider() {
/**
* Search for items matching the given query
* @param query The query to search for
*/
abstract suspend fun search(query: String, allowNetwork: Boolean): List<T>
/**
* Get an item by its id.
* This only needs to be implemented if `config.storageStrategy` is set to `StoreReference`
*/
open suspend fun get(id: String): T? {
return null
}
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
return query(uri, projection, null, null)
}
override fun query(
uri: Uri,
projection: Array<out String>?,
queryArgs: Bundle?,
cancellationSignal: CancellationSignal?
): Cursor? {
val context = context ?: return null
checkPermissionOrThrow(context)
when {
uri.pathSegments.size == 1 && uri.pathSegments.first() == SearchPluginContract.Paths.Search -> {
val query =
uri.getQueryParameter(SearchPluginContract.Paths.QueryParam) ?: return null
val allowNetwork =
uri.getQueryParameter(SearchPluginContract.Paths.AllowNetworkParam)?.toBoolean()
?: false
val results = search(query, allowNetwork, cancellationSignal)
val cursor = createCursor(results.size)
for (result in results) {
writeToCursor(cursor, result)
}
return cursor
}
uri.pathSegments.size == 2 && uri.pathSegments.first() == SearchPluginContract.Paths.Root -> {
val id = uri.pathSegments[1]
val result = runBlocking {
get(id)
}
return if (result != null) {
val cursor = createCursor(1)
writeToCursor(cursor, result)
cursor
} else {
createCursor(0)
}
}
}
return null
}
override fun getType(uri: Uri): String? {
throw UnsupportedOperationException("This operation is not supported")
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException("This operation is not supported")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
throw UnsupportedOperationException("This operation is not supported")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
throw UnsupportedOperationException("This operation is not supported")
}
private fun search(
query: String,
allowNetwork: Boolean,
cancellationSignal: CancellationSignal?
): List<T> {
return launchWithCancellationSignal(cancellationSignal) {
search(query, allowNetwork)
}
}
final override fun getPluginConfig(): Bundle {
return config.toBundle()
}
internal abstract fun createCursor(capacity: Int): MatrixCursor
internal abstract fun writeToCursor(cursor: MatrixCursor, item: T)
}
@@ -0,0 +1,14 @@
package de.mm20.launcher2.sdk.base
import android.net.Uri
import de.mm20.launcher2.plugin.config.QueryPluginConfig
import de.mm20.launcher2.plugin.contracts.SearchPluginContract
abstract class StringPluginProvider<T>(
config: QueryPluginConfig,
) : QueryPluginProvider<String, T>(config) {
override fun getQuery(uri: Uri): String? {
return uri.getQueryParameter(SearchPluginContract.Paths.QueryParam)
}
}
@@ -1,9 +1,9 @@
package de.mm20.launcher2.sdk.config
import android.os.Bundle
import de.mm20.launcher2.plugin.config.SearchPluginConfig
import de.mm20.launcher2.plugin.config.QueryPluginConfig
internal fun SearchPluginConfig.toBundle(): Bundle {
internal fun QueryPluginConfig.toBundle(): Bundle {
return Bundle().apply {
putString("storageStrategy", storageStrategy.name)
}
@@ -1,70 +1,41 @@
package de.mm20.launcher2.sdk.files
import android.database.MatrixCursor
import android.database.Cursor
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugin.config.SearchPluginConfig
import de.mm20.launcher2.plugin.contracts.FilePluginContract
import de.mm20.launcher2.sdk.base.SearchPluginProvider
import de.mm20.launcher2.plugin.config.QueryPluginConfig
import de.mm20.launcher2.plugin.contracts.FilePluginContract.FileColumns
import de.mm20.launcher2.plugin.data.buildCursor
import de.mm20.launcher2.sdk.base.StringPluginProvider
abstract class FileProvider(
config: SearchPluginConfig,
) : SearchPluginProvider<File>(config) {
abstract override suspend fun search(query: String, allowNetwork: Boolean): List<File>
config: QueryPluginConfig,
) : StringPluginProvider<File>(config) {
final override fun getPluginType(): PluginType {
return PluginType.FileSearch
}
override fun createCursor(capacity: Int): MatrixCursor {
return MatrixCursor(
arrayOf(
FilePluginContract.FileColumns.Id,
FilePluginContract.FileColumns.DisplayName,
FilePluginContract.FileColumns.MimeType,
FilePluginContract.FileColumns.Size,
FilePluginContract.FileColumns.Path,
FilePluginContract.FileColumns.ContentUri,
FilePluginContract.FileColumns.ThumbnailUri,
FilePluginContract.FileColumns.IsDirectory,
FilePluginContract.FileColumns.Owner,
FilePluginContract.FileColumns.MetaTitle,
FilePluginContract.FileColumns.MetaArtist,
FilePluginContract.FileColumns.MetaAlbum,
FilePluginContract.FileColumns.MetaDuration,
FilePluginContract.FileColumns.MetaYear,
FilePluginContract.FileColumns.MetaWidth,
FilePluginContract.FileColumns.MetaHeight,
FilePluginContract.FileColumns.MetaLocation,
FilePluginContract.FileColumns.MetaAppName,
FilePluginContract.FileColumns.MetaAppPackageName,
),
capacity,
)
}
override fun writeToCursor(cursor: MatrixCursor, item: File) {
cursor.addRow(
arrayOf(
item.id,
item.displayName,
item.mimeType,
item.size,
item.path,
item.uri.toString(),
item.thumbnailUri?.toString(),
if (item.isDirectory) 1 else 0,
item.owner,
item.metadata.title,
item.metadata.artist,
item.metadata.album,
item.metadata.duration,
item.metadata.year,
item.metadata.dimensions?.width,
item.metadata.dimensions?.height,
item.metadata.location,
item.metadata.appName,
item.metadata.appPackageName,
)
)
override fun List<File>.toCursor(): Cursor {
return buildCursor(FileColumns, this) {
put(FileColumns.Id, it.id)
put(FileColumns.DisplayName, it.displayName)
put(FileColumns.MimeType, it.mimeType)
put(FileColumns.Size, it.size)
put(FileColumns.Path, it.path)
put(FileColumns.ContentUri, it.uri.toString())
put(FileColumns.ThumbnailUri, it.thumbnailUri?.toString())
put(FileColumns.IsDirectory, it.isDirectory)
put(FileColumns.Owner, it.owner)
put(FileColumns.MetaTitle, it.metadata.title)
put(FileColumns.MetaArtist, it.metadata.artist)
put(FileColumns.MetaAlbum, it.metadata.album)
put(FileColumns.MetaDuration, it.metadata.duration)
put(FileColumns.MetaYear, it.metadata.year)
put(FileColumns.MetaWidth, it.metadata.dimensions?.width)
put(FileColumns.MetaHeight, it.metadata.dimensions?.height)
put(FileColumns.MetaLocation, it.metadata.location)
put(FileColumns.MetaAppName, it.metadata.appName)
put(FileColumns.MetaAppPackageName, it.metadata.appPackageName)
}
}
}
@@ -0,0 +1,37 @@
package de.mm20.launcher2.sdk.locations
import de.mm20.launcher2.search.location.Address
import de.mm20.launcher2.search.location.Attribution
import de.mm20.launcher2.search.location.Departure
import de.mm20.launcher2.search.location.LocationIcon
import de.mm20.launcher2.search.location.OpeningSchedule
data class Location(
val id: String,
val label: String,
val latitude: Double,
val longitude: Double,
val icon: LocationIcon? = null,
/**
* Human-readable category of the location.
* Should be localized.
*/
val category: String?,
val address: Address? = null,
val openingSchedule: OpeningSchedule? = null,
val websiteUrl: String? = null,
val phoneNumber: String? = null,
val emailAddress: String? = null,
/**
* User rating of a location, from 0 to 1.
* Will be multiplied by 5 to get a 5-star rating.
*/
val userRating: Float? = null,
/**
* Number of reviews that were used to calculate the user rating.
*/
val userRatingCount: Int? = null,
val departures: List<Departure>? = null,
val fixMeUrl: String? = null,
val attribution: Attribution? = null,
)
@@ -0,0 +1,80 @@
package de.mm20.launcher2.sdk.locations
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugin.config.QueryPluginConfig
import de.mm20.launcher2.plugin.contracts.LocationPluginContract
import de.mm20.launcher2.plugin.contracts.LocationPluginContract.LocationColumns
import de.mm20.launcher2.plugin.data.buildCursor
import de.mm20.launcher2.plugin.data.get
import de.mm20.launcher2.sdk.base.QueryPluginProvider
import de.mm20.launcher2.serialization.Json
abstract class LocationProvider(
config: QueryPluginConfig,
) : QueryPluginProvider<LocationQuery, Location>(config) {
private val json = Json.Lenient
final override fun getPluginType(): PluginType {
return PluginType.LocationSearch
}
override fun List<Location>.toCursor(): Cursor {
return buildCursor(LocationColumns, this) {
put(LocationColumns.Id, it.id)
put(LocationColumns.Label, it.label)
put(LocationColumns.Latitude, it.latitude)
put(LocationColumns.Longitude, it.longitude)
put(LocationColumns.FixMeUrl, it.fixMeUrl)
put(LocationColumns.Icon, it.icon)
put(LocationColumns.Category, it.category)
put(LocationColumns.Address, it.address)
put(LocationColumns.OpeningSchedule, it.openingSchedule)
put(LocationColumns.WebsiteUrl, it.websiteUrl)
put(LocationColumns.PhoneNumber, it.phoneNumber)
put(LocationColumns.EmailAddress, it.emailAddress)
put(LocationColumns.UserRating, it.userRating)
put(LocationColumns.UserRatingCount, it.userRatingCount)
put(LocationColumns.Departures, it.departures)
put(LocationColumns.Attribution, it.attribution)
}
}
override fun getQuery(uri: Uri): LocationQuery? {
val query = uri.getQueryParameter(LocationPluginContract.Params.Query) ?: return null
val searchRadius = uri.getQueryParameter(LocationPluginContract.Params.SearchRadius)?.toLongOrNull() ?: return null
val lat = uri.getQueryParameter(LocationPluginContract.Params.UserLatitude)?.toDoubleOrNull() ?: return null
val lon = uri.getQueryParameter(LocationPluginContract.Params.UserLongitude)?.toDoubleOrNull() ?: return null
return LocationQuery(
query = query,
userLatitude = lat,
userLongitude = lon,
searchRadius = searchRadius,
)
}
final override fun Bundle.toResult(): Location? {
return Location(
id = get(LocationColumns.Id) ?: return null,
label = get(LocationColumns.Label) ?: return null,
latitude = get(LocationColumns.Latitude) ?: return null,
longitude = get(LocationColumns.Longitude) ?:return null,
fixMeUrl = get(LocationColumns.FixMeUrl),
icon = get(LocationColumns.Icon),
category = get(LocationColumns.Category),
address = get(LocationColumns.Address),
openingSchedule = get(LocationColumns.OpeningSchedule),
websiteUrl = get(LocationColumns.WebsiteUrl),
phoneNumber = get(LocationColumns.PhoneNumber),
emailAddress = get(LocationColumns.EmailAddress),
userRating = get(LocationColumns.UserRating),
userRatingCount = get(LocationColumns.UserRatingCount),
departures = get(LocationColumns.Departures),
attribution = get(LocationColumns.Attribution),
)
}
}
@@ -0,0 +1,8 @@
package de.mm20.launcher2.sdk.locations
data class LocationQuery(
val query: String,
val userLatitude: Double,
val userLongitude: Double,
val searchRadius: Long,
)
@@ -99,31 +99,8 @@ val Double.mm
val Double.inch
get() = Precipitation(this * 25.4)
enum class WeatherIcon {
Unknown,
Clear,
Cloudy,
Cold,
Drizzle,
Haze,
Fog,
Hail,
HeavyThunderstorm,
HeavyThunderstormWithRain,
Hot,
MostlyCloudy,
PartlyCloudy,
Showers,
Sleet,
Snow,
Storm,
Thunderstorm,
ThunderstormWithRain,
Wind,
BrokenClouds,
}
@Deprecated("Use de.mm20.launcher2.weather.WeatherIcon")
typealias WeatherIcon = de.mm20.launcher2.weather.WeatherIcon
data class Forecast(
/**
@@ -2,7 +2,6 @@ package de.mm20.launcher2.sdk.weather
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.location.Geocoder
import android.net.Uri
import android.os.Bundle
@@ -11,6 +10,9 @@ import android.util.Log
import de.mm20.launcher2.plugin.PluginType
import de.mm20.launcher2.plugin.config.WeatherPluginConfig
import de.mm20.launcher2.plugin.contracts.WeatherPluginContract
import de.mm20.launcher2.plugin.contracts.WeatherPluginContract.ForecastColumns
import de.mm20.launcher2.plugin.contracts.WeatherPluginContract.LocationColumns
import de.mm20.launcher2.plugin.data.buildCursor
import de.mm20.launcher2.sdk.base.BasePluginProvider
import de.mm20.launcher2.sdk.config.toBundle
import de.mm20.launcher2.sdk.ktx.formatToString
@@ -71,7 +73,26 @@ abstract class WeatherProvider(
val forecasts = launchWithCancellationSignal(cancellationSignal) {
getWeatherData(lat, lon, id, name, lang)
} ?: return null
return createForecastCursor(forecasts)
return buildCursor(ForecastColumns, forecasts) {
put(ForecastColumns.Timestamp, it.timestamp)
put(ForecastColumns.CreatedAt, it.createdAt)
put(ForecastColumns.Temperature, it.temperature.kelvin)
put(ForecastColumns.TemperatureMin, it.minTemp?.kelvin)
put(ForecastColumns.TemperatureMax, it.maxTemp?.kelvin)
put(ForecastColumns.Pressure, it.pressure?.hPa)
put(ForecastColumns.Humidity, it.humidity)
put(ForecastColumns.WindSpeed, it.windSpeed?.metersPerSecond)
put(ForecastColumns.WindDirection, it.windDirection)
put(ForecastColumns.Precipitation, it.precipitation?.mm)
put(ForecastColumns.RainProbability, it.rainProbability)
put(ForecastColumns.Clouds, it.clouds)
put(ForecastColumns.Location, it.location)
put(ForecastColumns.Provider, it.provider)
put(ForecastColumns.ProviderUrl, it.providerUrl)
put(ForecastColumns.Night, it.night)
put(ForecastColumns.Icon, it.icon)
put(ForecastColumns.Condition, it.condition)
}
}
uri.pathSegments.size == 1 && uri.pathSegments.first() == WeatherPluginContract.Paths.Locations -> {
@@ -85,7 +106,16 @@ abstract class WeatherProvider(
) {
findLocations(query, lang)
}
return createLocationsCursor(locations)
return buildCursor(LocationColumns, locations) {
if (it is WeatherLocation.Id) {
put(LocationColumns.Id, it.id)
put(LocationColumns.Name, it.name)
} else if (it is WeatherLocation.LatLon) {
put(LocationColumns.Lat, it.lat)
put(LocationColumns.Lon, it.lon)
put(LocationColumns.Name, it.name)
}
}
}
}
return null
@@ -113,90 +143,6 @@ abstract class WeatherProvider(
return null
}
private fun createForecastCursor(forecasts: List<Forecast>): Cursor {
val cursor = MatrixCursor(
arrayOf(
WeatherPluginContract.ForecastColumns.Timestamp,
WeatherPluginContract.ForecastColumns.CreatedAt,
WeatherPluginContract.ForecastColumns.Temperature,
WeatherPluginContract.ForecastColumns.TemperatureMin,
WeatherPluginContract.ForecastColumns.TemperatureMax,
WeatherPluginContract.ForecastColumns.Pressure,
WeatherPluginContract.ForecastColumns.Humidity,
WeatherPluginContract.ForecastColumns.WindSpeed,
WeatherPluginContract.ForecastColumns.WindDirection,
WeatherPluginContract.ForecastColumns.Precipitation,
WeatherPluginContract.ForecastColumns.RainProbability,
WeatherPluginContract.ForecastColumns.Clouds,
WeatherPluginContract.ForecastColumns.Location,
WeatherPluginContract.ForecastColumns.Provider,
WeatherPluginContract.ForecastColumns.ProviderUrl,
WeatherPluginContract.ForecastColumns.Night,
WeatherPluginContract.ForecastColumns.Icon,
WeatherPluginContract.ForecastColumns.Condition,
),
forecasts.size,
)
for (forecast in forecasts) {
cursor.addRow(
arrayOf(
forecast.timestamp,
forecast.createdAt,
forecast.temperature.kelvin,
forecast.minTemp?.kelvin,
forecast.maxTemp?.kelvin,
forecast.pressure?.hPa,
forecast.humidity,
forecast.windSpeed?.metersPerSecond,
forecast.windDirection,
forecast.precipitation?.mm,
forecast.rainProbability,
forecast.clouds,
forecast.location,
forecast.provider,
forecast.providerUrl,
if (forecast.night) 1 else 0,
forecast.icon.name,
forecast.condition,
)
)
}
return cursor
}
fun createLocationsCursor(locations: List<WeatherLocation>): Cursor {
val cursor = MatrixCursor(
arrayOf(
WeatherPluginContract.LocationColumns.Id,
WeatherPluginContract.LocationColumns.Lat,
WeatherPluginContract.LocationColumns.Lon,
WeatherPluginContract.LocationColumns.Name,
),
locations.size,
)
for (location in locations) {
if (location is WeatherLocation.Id) {
cursor.addRow(
arrayOf(
location.id,
null,
null,
location.name,
)
)
} else if (location is WeatherLocation.LatLon) {
cursor.addRow(
arrayOf(
null,
location.lat,
location.lon,
location.name,
)
)
}
}
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException("This operation is not supported")