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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user