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:
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.plugin.serialization)
|
||||
alias(libs.plugins.dokka)
|
||||
`maven-publish`
|
||||
signing
|
||||
@@ -46,6 +47,11 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.bundles.kotlin)
|
||||
}
|
||||
|
||||
tasks.dokkaHtml {
|
||||
outputDirectory.set(layout.buildDirectory.dir("dokka"))
|
||||
}
|
||||
@@ -95,12 +101,16 @@ publishing {
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/MM2-0/Kvaesitso")
|
||||
credentials {
|
||||
username = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
|
||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
|
||||
val ghUser = project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
|
||||
if (ghUser == "MM2-0") {
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/MM2-0/Kvaesitso")
|
||||
credentials {
|
||||
username =
|
||||
project.findProperty("gpr.user") as String? ?: System.getenv("USERNAME")
|
||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ package de.mm20.launcher2.plugin
|
||||
enum class PluginType {
|
||||
FileSearch,
|
||||
Weather,
|
||||
LocationSearch,
|
||||
}
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
package de.mm20.launcher2.plugin.config
|
||||
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.plugin.config.StorageStrategy
|
||||
|
||||
data class SearchPluginConfig(
|
||||
data class QueryPluginConfig(
|
||||
/**
|
||||
* Strategy to store items from this provider in the launcher database
|
||||
* @see [StorageStrategy]
|
||||
@@ -1,9 +1,13 @@
|
||||
package de.mm20.launcher2.plugin.config
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Defines how the launcher should store search results from a plugin (i.e. when the search result is
|
||||
* added to favorites).
|
||||
*/
|
||||
@Serializable
|
||||
enum class StorageStrategy {
|
||||
/**
|
||||
* The launcher only stores the ID and the plugin provider for the search result. To restore the
|
||||
@@ -13,6 +17,7 @@ enum class StorageStrategy {
|
||||
* particular, the plugin provider must be able to restore a search result without any network
|
||||
* requests.
|
||||
*/
|
||||
@SerialName("ref")
|
||||
StoreReference,
|
||||
|
||||
/**
|
||||
@@ -21,13 +26,6 @@ enum class StorageStrategy {
|
||||
* Use this strategy if your plugin needs to perform network requests to retrieve search
|
||||
* results and if you don't want to implement a cache for search results.
|
||||
*/
|
||||
@SerialName("copy")
|
||||
StoreCopy,
|
||||
|
||||
/**
|
||||
* 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].
|
||||
*/
|
||||
Deferred,
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
import android.database.Cursor
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.plugin.data.RowBuilderScope
|
||||
import de.mm20.launcher2.serialization.Json
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
abstract class Columns {
|
||||
internal val columns = mutableSetOf<String>()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal inline fun <reified T : Any> Columns.column(name: String): Column<T> {
|
||||
val column = when (T::class) {
|
||||
Int::class -> IntColumn(name)
|
||||
Long::class -> LongColumn(name)
|
||||
String::class -> StringColumn(name)
|
||||
Short::class -> ShortColumn(name)
|
||||
Boolean::class -> BooleanColumn(name)
|
||||
Double::class -> DoubleColumn(name)
|
||||
Float::class -> FloatColumn(name)
|
||||
ByteArray::class -> BlobColumn(name)
|
||||
else -> {
|
||||
if (T::class.java.isEnum) {
|
||||
SerializableColumn(
|
||||
name,
|
||||
{ it.toString() },
|
||||
{ v -> T::class.java.enumConstants?.find { it.toString() == v } }
|
||||
)
|
||||
} else {
|
||||
SerializableColumn(
|
||||
name,
|
||||
{ Json.Lenient.encodeToString(it) },
|
||||
{ Json.Lenient.decodeFromString(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
} as Column<T>
|
||||
columns.add(name)
|
||||
return column
|
||||
}
|
||||
|
||||
sealed interface Column<T> {
|
||||
val name: String
|
||||
fun Cursor.readAtIndex(index: Int): T?
|
||||
fun RowBuilderScope.toCursorValue(value: T?): Any? {
|
||||
return value
|
||||
}
|
||||
|
||||
fun Bundle.put(value: T?)
|
||||
fun Bundle.get(): T?
|
||||
}
|
||||
|
||||
internal class IntColumn(
|
||||
override val name: String
|
||||
) : Column<Int> {
|
||||
override fun Cursor.readAtIndex(index: Int): Int {
|
||||
return getInt(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: Int?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putInt(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): Int {
|
||||
return getInt(name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class LongColumn(
|
||||
override val name: String
|
||||
) : Column<Long> {
|
||||
override fun Cursor.readAtIndex(index: Int): Long {
|
||||
return getLong(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: Long?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putLong(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): Long {
|
||||
return getLong(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal class DoubleColumn(
|
||||
override val name: String
|
||||
) : Column<Double> {
|
||||
override fun Cursor.readAtIndex(index: Int): Double {
|
||||
return getDouble(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: Double?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putDouble(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): Double {
|
||||
return getDouble(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal class FloatColumn(
|
||||
override val name: String
|
||||
) : Column<Float> {
|
||||
override fun Cursor.readAtIndex(index: Int): Float {
|
||||
return getFloat(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: Float?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putFloat(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): Float {
|
||||
return getFloat(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal class StringColumn(
|
||||
override val name: String
|
||||
) : Column<String> {
|
||||
override fun Cursor.readAtIndex(index: Int): String? {
|
||||
return getString(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: String?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putString(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): String? {
|
||||
return getString(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ShortColumn(
|
||||
override val name: String
|
||||
) : Column<Short> {
|
||||
override fun Cursor.readAtIndex(index: Int): Short {
|
||||
return getShort(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: Short?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putShort(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): Short {
|
||||
return getShort(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal class BooleanColumn(
|
||||
override val name: String
|
||||
) : Column<Boolean> {
|
||||
override fun Cursor.readAtIndex(index: Int): Boolean {
|
||||
return getInt(index) != 0
|
||||
}
|
||||
|
||||
override fun RowBuilderScope.toCursorValue(value: Boolean?): Any? {
|
||||
value ?: return null
|
||||
return if (value) 1 else 0
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: Boolean?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putBoolean(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): Boolean? {
|
||||
return getBoolean(name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class BlobColumn(
|
||||
override val name: String
|
||||
) : Column<ByteArray> {
|
||||
|
||||
override fun Cursor.readAtIndex(index: Int): ByteArray? {
|
||||
return getBlob(index)
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: ByteArray?) {
|
||||
if (value == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putByteArray(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): ByteArray? {
|
||||
return getByteArray(name)
|
||||
}
|
||||
}
|
||||
|
||||
internal class SerializableColumn<T>(
|
||||
override val name: String,
|
||||
val serialize: (T) -> String,
|
||||
val deserialize: (String) -> T
|
||||
) : Column<T> {
|
||||
override fun Cursor.readAtIndex(index: Int): T? {
|
||||
val string = getString(index)
|
||||
try {
|
||||
return deserialize(string)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Failed to read column value", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun RowBuilderScope.toCursorValue(value: T?): Any? {
|
||||
return try {
|
||||
serialize(value ?: return null)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Failed to write column value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.put(value: T?) {
|
||||
val serialized = try {
|
||||
value?.let(serialize)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Failed to serialize column value", e)
|
||||
null
|
||||
}
|
||||
if (serialized == null) {
|
||||
remove(name)
|
||||
} else {
|
||||
putString(name, serialized)
|
||||
}
|
||||
}
|
||||
|
||||
override fun Bundle.get(): T? {
|
||||
val string = getString(name) ?: return null
|
||||
return try {
|
||||
deserialize(string)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Failed to deserialize column value", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-21
@@ -2,64 +2,63 @@ package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
abstract class FilePluginContract {
|
||||
|
||||
object FileColumns {
|
||||
object FileColumns: Columns() {
|
||||
/**
|
||||
* The unique ID of the file.
|
||||
* Type: String
|
||||
*/
|
||||
const val Id = "id"
|
||||
val Id = column<String>("id")
|
||||
|
||||
/**
|
||||
* The display name of the file.
|
||||
* Type: String
|
||||
*/
|
||||
const val DisplayName = "display_name"
|
||||
val DisplayName = column<String>("display_name")
|
||||
|
||||
/**
|
||||
* The MIME type of the file. This is used to determine how to open the file. Make sure that
|
||||
* this is either a common MIME type or that your app can handle this MIME type.
|
||||
* Type: String?
|
||||
*/
|
||||
const val MimeType = "mime_type"
|
||||
val MimeType = column<String>("mime_type")
|
||||
|
||||
/**
|
||||
* The size of the file in bytes.
|
||||
* Type: Long?
|
||||
*/
|
||||
const val Size = "size"
|
||||
val Size = column<Long>("size")
|
||||
|
||||
/**
|
||||
* The display path of the file.
|
||||
* Type: String?
|
||||
*/
|
||||
const val Path = "path"
|
||||
val Path = column<String>("path")
|
||||
|
||||
/**
|
||||
* The URI to view the file.
|
||||
* Type: String?
|
||||
*/
|
||||
const val ContentUri = "uri"
|
||||
val ContentUri = column<String>("uri")
|
||||
|
||||
const val ThumbnailUri = "thumbnail_uri"
|
||||
val ThumbnailUri = column<String>("thumbnail_uri")
|
||||
|
||||
/**
|
||||
* Whether the file is a directory.
|
||||
* Type: Int
|
||||
*/
|
||||
const val IsDirectory = "is_directory"
|
||||
val IsDirectory = column<Boolean>("is_directory")
|
||||
|
||||
const val Owner = "owner"
|
||||
val Owner = column<String>("owner")
|
||||
|
||||
const val MetaTitle = "meta_title"
|
||||
const val MetaArtist = "meta_artist"
|
||||
const val MetaAlbum = "meta_album"
|
||||
const val MetaDuration = "meta_duration"
|
||||
const val MetaYear = "meta_year"
|
||||
const val MetaWidth = "meta_width"
|
||||
const val MetaHeight = "meta_height"
|
||||
const val MetaLocation = "meta_location"
|
||||
const val MetaAppName = "meta_app_name"
|
||||
const val MetaAppPackageName = "meta_app_package_name"
|
||||
const val MetaAppMinSdkVersion = "meta_app_min_sdk_version"
|
||||
val MetaTitle = column<String>("meta_title")
|
||||
val MetaArtist = column<String>("meta_artist")
|
||||
val MetaAlbum = column<String>("meta_album")
|
||||
val MetaDuration = column<Long>("meta_duration")
|
||||
val MetaYear = column<Int>("meta_year")
|
||||
val MetaWidth = column<Int>("meta_width")
|
||||
val MetaHeight = column<Int>("meta_height")
|
||||
val MetaLocation = column<String>("meta_location")
|
||||
val MetaAppName = column<String>("meta_app_name")
|
||||
val MetaAppPackageName = column<String>("meta_app_package_name")
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
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
|
||||
|
||||
abstract class LocationPluginContract {
|
||||
object Paths {
|
||||
const val Search = "search"
|
||||
const val Get = "get"
|
||||
}
|
||||
|
||||
object Params {
|
||||
/**
|
||||
* Search query.
|
||||
* Type: String
|
||||
*/
|
||||
const val Query = "query"
|
||||
|
||||
/**
|
||||
* Latitude of user's current location in degrees.
|
||||
* Type: Double?
|
||||
*/
|
||||
const val UserLatitude = "user_latitude"
|
||||
|
||||
/**
|
||||
* Longitude of user's current location in degrees.
|
||||
* Type: Double?
|
||||
*/
|
||||
const val UserLongitude = "user_longitude"
|
||||
|
||||
/**
|
||||
* Search radius in meters.
|
||||
* Type: Long
|
||||
*/
|
||||
const val SearchRadius = "search_radius"
|
||||
|
||||
/**
|
||||
* Whether to allow network requests.
|
||||
* Type: Boolean
|
||||
*/
|
||||
const val AllowNetwork = "network"
|
||||
}
|
||||
|
||||
object GetParams {
|
||||
/**
|
||||
* Unique identifier of location to look up.
|
||||
* Type: String
|
||||
*/
|
||||
const val Id = "id"
|
||||
}
|
||||
|
||||
object LocationColumns : Columns() {
|
||||
/**
|
||||
* Unique identifier of location.
|
||||
* Type: String
|
||||
*/
|
||||
val Id = column<String>("id")
|
||||
|
||||
/**
|
||||
* Display name of location.
|
||||
* Type: String
|
||||
*/
|
||||
val Label = column<String>("label")
|
||||
|
||||
/**
|
||||
* Latitude of location in degrees.
|
||||
* Type: Double
|
||||
*/
|
||||
val Latitude = column<Double>("latitude")
|
||||
|
||||
/**
|
||||
* Longitude of location in degrees.
|
||||
* Type: Double
|
||||
*/
|
||||
val Longitude = column<Double>("longitude")
|
||||
|
||||
/**
|
||||
* Url for users to report / fix incorrect data.
|
||||
* Type: String?
|
||||
*/
|
||||
val FixMeUrl = column<String>("fix_me_url")
|
||||
|
||||
/**
|
||||
* Icon of location.
|
||||
* Type: String? (enum LocationIcon)
|
||||
*/
|
||||
val Icon = column<LocationIcon>("icon")
|
||||
|
||||
/**
|
||||
* Location category.
|
||||
* Type: String?
|
||||
*/
|
||||
val Category = column<String>("category")
|
||||
|
||||
/**
|
||||
* Street name of location.
|
||||
* Type: String? (JSON)
|
||||
*/
|
||||
val Address = column<Address>("address")
|
||||
|
||||
/**
|
||||
* Opening schedule of location, encoded as JSON.
|
||||
* Type: String? (JSON)
|
||||
*/
|
||||
val OpeningSchedule = column<OpeningSchedule>("opening_schedule")
|
||||
|
||||
/**
|
||||
* Website URL of location.
|
||||
* Type: String?
|
||||
*/
|
||||
val WebsiteUrl = column<String>("website_url")
|
||||
|
||||
/**
|
||||
* Phone number of location.
|
||||
* Type: String?
|
||||
*/
|
||||
val PhoneNumber = column<String>("phone_number")
|
||||
|
||||
val EmailAddress = column<String>("email_address")
|
||||
|
||||
/**
|
||||
* User rating of location, from 0.0 (worst) to 1.0 (best)
|
||||
* Type: Float?
|
||||
*/
|
||||
val UserRating = column<Float>("user_rating")
|
||||
val UserRatingCount = column<Int>("user_rating_count")
|
||||
|
||||
/**
|
||||
* Public transport departures originating from this location, encoded as JSON.
|
||||
* Type: String? (JSON)
|
||||
*/
|
||||
val Departures = column<List<Departure>>("departures")
|
||||
|
||||
/**
|
||||
* Attribution information for location data.
|
||||
* Type: String? (JSON)
|
||||
*/
|
||||
val Attribution = column<Attribution>("attribution")
|
||||
}
|
||||
}
|
||||
+16
-2
@@ -4,7 +4,21 @@ abstract class SearchPluginContract {
|
||||
object Paths {
|
||||
const val Search = "search"
|
||||
const val Root = "root"
|
||||
const val QueryParam = "query"
|
||||
const val AllowNetworkParam = "network"
|
||||
const val Refresh = "refresh"
|
||||
@Deprecated("Use Paths.Query instead")
|
||||
const val QueryParam = Params.Query
|
||||
@Deprecated("Use Params.AllowNetwork instead")
|
||||
const val AllowNetworkParam = Params.AllowNetwork
|
||||
@Deprecated("Use Params.Lang instead")
|
||||
const val LangParam = Params.Lang
|
||||
}
|
||||
object Params {
|
||||
const val AllowNetwork = "network"
|
||||
const val Lang = "lang"
|
||||
const val UpdatedAt = "updatedAt"
|
||||
const val Query = "query"
|
||||
}
|
||||
object Extras {
|
||||
const val NotUpdated = "notUpdated"
|
||||
}
|
||||
}
|
||||
+26
-24
@@ -1,5 +1,7 @@
|
||||
package de.mm20.launcher2.plugin.contracts
|
||||
|
||||
import de.mm20.launcher2.weather.WeatherIcon
|
||||
|
||||
object WeatherPluginContract {
|
||||
object Paths {
|
||||
const val Forecasts = "forecasts"
|
||||
@@ -14,25 +16,25 @@ object WeatherPluginContract {
|
||||
const val Language = "lang"
|
||||
}
|
||||
|
||||
object ForecastColumns {
|
||||
const val Timestamp = "timestamp"
|
||||
const val CreatedAt = "created_at"
|
||||
const val Temperature = "temperature"
|
||||
const val TemperatureMin = "temperature_min"
|
||||
const val TemperatureMax = "temperature_max"
|
||||
const val Pressure = "pressure"
|
||||
const val Humidity = "humidity"
|
||||
const val WindSpeed = "wind_speed"
|
||||
const val WindDirection = "wind_direction"
|
||||
const val Precipitation = "precipitation"
|
||||
const val RainProbability = "rain_probability"
|
||||
const val Clouds = "clouds"
|
||||
const val Location = "location"
|
||||
const val Provider = "provider"
|
||||
const val ProviderUrl = "provider_url"
|
||||
const val Night = "night"
|
||||
const val Icon = "icon"
|
||||
const val Condition = "condition"
|
||||
object ForecastColumns : Columns() {
|
||||
val Timestamp = column<Long>("timestamp")
|
||||
val CreatedAt = column<Long>("created_at")
|
||||
val Temperature = column<Double>("temperature")
|
||||
val TemperatureMin = column<Double>("temperature_min")
|
||||
val TemperatureMax = column<Double>("temperature_max")
|
||||
val Pressure = column<Double>("pressure")
|
||||
val Humidity = column<Int>("humidity")
|
||||
val WindSpeed = column<Double>("wind_speed")
|
||||
val WindDirection = column<Double>("wind_direction")
|
||||
val Precipitation = column<Double>("precipitation")
|
||||
val RainProbability = column<Int>("rain_probability")
|
||||
val Clouds = column<Int>("clouds")
|
||||
val Location = column<String>("location")
|
||||
val Provider = column<String>("provider")
|
||||
val ProviderUrl = column<String>("provider_url")
|
||||
val Night = column<Boolean>("night")
|
||||
val Icon = column<WeatherIcon>("icon")
|
||||
val Condition = column<String>("condition")
|
||||
}
|
||||
|
||||
object LocationParams {
|
||||
@@ -40,10 +42,10 @@ object WeatherPluginContract {
|
||||
const val Language = "lang"
|
||||
}
|
||||
|
||||
object LocationColumns {
|
||||
const val Id = "id"
|
||||
const val Lat = "lat"
|
||||
const val Lon = "lon"
|
||||
const val Name = "name"
|
||||
object LocationColumns : Columns() {
|
||||
val Id = column<String>("id")
|
||||
val Lat = column<Double>("lat")
|
||||
val Lon = column<Double>("lon")
|
||||
val Name = column<String>("name")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.mm20.launcher2.plugin.data
|
||||
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.plugin.contracts.Column
|
||||
|
||||
operator fun <T> Bundle.get(column: Column<T>): T? {
|
||||
return with(column) {
|
||||
if (!containsKey(column.name)) return null
|
||||
get()
|
||||
}
|
||||
}
|
||||
|
||||
operator fun <T> Bundle.set(column: Column<T>, value: T?) {
|
||||
with(column) {
|
||||
put(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.mm20.launcher2.plugin.data
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.plugin.contracts.Column
|
||||
import de.mm20.launcher2.plugin.contracts.Columns
|
||||
|
||||
interface ColumnsScope {
|
||||
operator fun <T : Any> Cursor.get(column: Column<T>): T?
|
||||
operator fun Cursor.contains(column: Column<*>): Boolean
|
||||
}
|
||||
|
||||
internal class ColumnsScopeImpl(
|
||||
private val columnIndices: Map<String, Int>,
|
||||
private val cursor: Cursor
|
||||
) : ColumnsScope {
|
||||
override operator fun <T : Any> Cursor.get(column: Column<T>): T? {
|
||||
val index = columnIndices[column.name] ?: return null
|
||||
if (index == -1) return null
|
||||
if (cursor.isNull(index)) return null
|
||||
try {
|
||||
return with(column) { cursor.readAtIndex(index) }
|
||||
} catch (e: Exception) {
|
||||
Log.e("MM20", "Failed to get column value", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun Cursor.contains(column: Column<*>): Boolean {
|
||||
return columnIndices.containsKey(column.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun Cursor.withColumns(columns: Columns, block: ColumnsScope.() -> Unit) {
|
||||
val scope = ColumnsScopeImpl(
|
||||
columns.columns.associateWith { name -> getColumnIndex(name) },
|
||||
this
|
||||
)
|
||||
scope.block()
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun <T> buildCursor(
|
||||
columns: Columns,
|
||||
data: List<T>,
|
||||
rowBuilder: RowBuilderScope.(item: T) -> Unit
|
||||
): Cursor {
|
||||
val cursor = MatrixCursor(columns.columns.toTypedArray(), data.size)
|
||||
val values = Array<Any?>(columns.columns.size) { null }
|
||||
val scope = RowBuilderScopeImpl(
|
||||
columnIndices = columns.columns.withIndex().associate { (index, name) -> name to index },
|
||||
values = values
|
||||
)
|
||||
repeat(data.size) {
|
||||
values.fill(null)
|
||||
scope.rowBuilder(data[it])
|
||||
cursor.addRow(scope.values)
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
interface RowBuilderScope {
|
||||
fun <T : Any> put(column: Column<T>, value: T?)
|
||||
}
|
||||
|
||||
internal class RowBuilderScopeImpl(
|
||||
private val columnIndices: Map<String, Int>,
|
||||
internal val values: Array<Any?>
|
||||
) : RowBuilderScope {
|
||||
override fun <T : Any> put(column: Column<T>, value: T?) {
|
||||
value ?: return
|
||||
val index = columnIndices[column.name] ?: return
|
||||
values[index] = with(column) { toCursorValue(value) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package de.mm20.launcher2.search.location
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Address(
|
||||
/**
|
||||
* Address line 1 (e.g. street name and number)
|
||||
*/
|
||||
val address: String? = null,
|
||||
/**
|
||||
* Address line 2 (e.g. apartment number)
|
||||
*/
|
||||
val address2: String? = null,
|
||||
/**
|
||||
* Address line 3 (e.g. additional information)
|
||||
*/
|
||||
val address3: String? = null,
|
||||
/**
|
||||
* City
|
||||
*/
|
||||
val city: String? = null,
|
||||
/**
|
||||
* State, province, or region
|
||||
*/
|
||||
val state: String? = null,
|
||||
/**
|
||||
* Postal code
|
||||
*/
|
||||
val postalCode: String? = null,
|
||||
/**
|
||||
* Country
|
||||
*/
|
||||
val country: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.mm20.launcher2.search.location
|
||||
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.serialization.UriSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Attribution(
|
||||
val text: String? = null,
|
||||
@Serializable(with = UriSerializer::class)
|
||||
val iconUrl: Uri? = null,
|
||||
val url: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.mm20.launcher2.search.location
|
||||
|
||||
import android.graphics.Color
|
||||
import de.mm20.launcher2.serialization.ColorSerializer
|
||||
import de.mm20.launcher2.serialization.DurationSerializer
|
||||
import de.mm20.launcher2.serialization.ZonedDateTimeSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Serializable
|
||||
data class Departure(
|
||||
/**
|
||||
* The scheduled time of the departure.
|
||||
*/
|
||||
@Serializable(with = ZonedDateTimeSerializer::class)
|
||||
val time: ZonedDateTime,
|
||||
/**
|
||||
* The delay of the departure.
|
||||
* `Duration.ZERO` if the departure is on time,
|
||||
* `null` if no real-time data is available.
|
||||
*/
|
||||
@Serializable(with = DurationSerializer::class)
|
||||
val delay: Duration?,
|
||||
/**
|
||||
* Name of the line (i.e. "11", "U2", "S1").
|
||||
*/
|
||||
val line: String,
|
||||
val lastStop: String?,
|
||||
val type: LineType? = null,
|
||||
@Serializable(with = ColorSerializer::class)
|
||||
val lineColor: Color?,
|
||||
)
|
||||
|
||||
enum class LineType {
|
||||
Bus,
|
||||
Tram,
|
||||
Subway,
|
||||
CommuterTrain,
|
||||
RegionalTrain,
|
||||
Train,
|
||||
HighSpeedTrain,
|
||||
Boat,
|
||||
Monorail,
|
||||
CableCar,
|
||||
Airplane,
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package de.mm20.launcher2.search.location
|
||||
|
||||
enum class LocationIcon {
|
||||
Car,
|
||||
CarRental,
|
||||
CarRepair,
|
||||
CarWash,
|
||||
ChargingStation,
|
||||
GasStation,
|
||||
GenericTransit,
|
||||
Parking,
|
||||
Bus,
|
||||
Tram,
|
||||
Train,
|
||||
Subway,
|
||||
CableCar,
|
||||
Airport,
|
||||
Boat,
|
||||
Taxi,
|
||||
Moped,
|
||||
Bike,
|
||||
Motorcycle,
|
||||
ElectricScooter,
|
||||
|
||||
ArtGallery,
|
||||
Museum,
|
||||
Theater,
|
||||
MovieTheater,
|
||||
AmusementPark,
|
||||
NightClub,
|
||||
ConcertHall,
|
||||
Stadium,
|
||||
Casino,
|
||||
Circus,
|
||||
|
||||
Hotel,
|
||||
|
||||
Restaurant,
|
||||
Cafe,
|
||||
FastFood,
|
||||
Pizza,
|
||||
Burger,
|
||||
Kebab,
|
||||
IceCream,
|
||||
Ramen,
|
||||
Soup,
|
||||
Bar,
|
||||
Brunch,
|
||||
Breakfast,
|
||||
Pub,
|
||||
JapaneseCuisine,
|
||||
AsianCuisine,
|
||||
|
||||
Shopping,
|
||||
Florist,
|
||||
Kiosk,
|
||||
FurnitureStore,
|
||||
CellPhoneStore,
|
||||
BookStore,
|
||||
ClothingStore,
|
||||
ConvenienceStore,
|
||||
DiscountStore,
|
||||
JewelryStore,
|
||||
LiquorStore,
|
||||
PetStore,
|
||||
ShoppingMall,
|
||||
Supermarket,
|
||||
Bakery,
|
||||
Optician,
|
||||
Pharmacy,
|
||||
HairSalon,
|
||||
Laundromat,
|
||||
|
||||
Sports,
|
||||
FitnessCenter,
|
||||
Soccer,
|
||||
Basketball,
|
||||
Tennis,
|
||||
Golf,
|
||||
Baseball,
|
||||
AmericanFootball,
|
||||
Hiking,
|
||||
Swimming,
|
||||
Surfing,
|
||||
Motorsports,
|
||||
Handball,
|
||||
Volleyball,
|
||||
Skiing,
|
||||
Kayaking,
|
||||
Skateboarding,
|
||||
Cricket,
|
||||
MartialArts,
|
||||
NordicWalking,
|
||||
Paragliding,
|
||||
Gymnastics,
|
||||
Snowboarding,
|
||||
Hockey,
|
||||
Rugby,
|
||||
|
||||
Bank,
|
||||
Atm,
|
||||
|
||||
Physician,
|
||||
Dentist,
|
||||
Hospital,
|
||||
Clinic,
|
||||
|
||||
Park,
|
||||
Forest,
|
||||
|
||||
Monument,
|
||||
Church,
|
||||
Mosque,
|
||||
Synagogue,
|
||||
BuddhistTemple,
|
||||
HinduTemple,
|
||||
Candle,
|
||||
|
||||
GovernmentBuilding,
|
||||
Police,
|
||||
FireDepartment,
|
||||
Courthouse,
|
||||
PostOffice,
|
||||
Library,
|
||||
School,
|
||||
University,
|
||||
PublicBathroom;
|
||||
|
||||
companion object {
|
||||
fun valueOfOrNull(string: String): LocationIcon? {
|
||||
return try {
|
||||
valueOf(string)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.search.location
|
||||
|
||||
import de.mm20.launcher2.serialization.DurationSerializer
|
||||
import de.mm20.launcher2.serialization.LocalTimeSerializer
|
||||
import de.mm20.launcher2.serialization.OpeningScheduleSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Duration
|
||||
import java.time.LocalTime
|
||||
|
||||
@Serializable
|
||||
data class OpeningHours(
|
||||
@JsonNames("day")
|
||||
val dayOfWeek: DayOfWeek,
|
||||
@JsonNames("openingTime")
|
||||
@Serializable(with = LocalTimeSerializer::class)
|
||||
val startTime: LocalTime,
|
||||
@Serializable(with = DurationSerializer::class)
|
||||
val duration: Duration
|
||||
) {
|
||||
override fun toString(): String = "$dayOfWeek $startTime-${startTime.plus(duration)}"
|
||||
}
|
||||
|
||||
@Serializable(with = OpeningScheduleSerializer::class)
|
||||
sealed interface OpeningSchedule {
|
||||
data object TwentyFourSeven : OpeningSchedule
|
||||
@Serializable
|
||||
data class Hours(@Serializable val openingHours: List<OpeningHours>) : OpeningSchedule
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import android.graphics.Color
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
object ColorSerializer: KSerializer<Color> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.INT)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Color {
|
||||
return Color.valueOf(decoder.decodeInt())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Color) {
|
||||
encoder.encodeInt(value.toArgb())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.DayOfWeek
|
||||
|
||||
object DayOfWeekSerializer : KSerializer<DayOfWeek> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.INT)
|
||||
|
||||
override fun deserialize(decoder: Decoder): DayOfWeek {
|
||||
return DayOfWeek.of(decoder.decodeInt())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: DayOfWeek) {
|
||||
encoder.encodeInt(value.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.Duration
|
||||
|
||||
object DurationSerializer: KSerializer<Duration> {
|
||||
override val descriptor = PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.LONG)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Duration) {
|
||||
encoder.encodeLong(value.toMillis())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Duration {
|
||||
return Duration.ofMillis(decoder.decodeLong())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
/**
|
||||
* Default Json serializer configurations
|
||||
*/
|
||||
object Json {
|
||||
/**
|
||||
* A Json serializer configuration that aims to be as forgiving as possible.
|
||||
* Suitable for external data sources, and legacy data that may not be fully compliant with the current schema.
|
||||
*/
|
||||
val Lenient = kotlinx.serialization.json.Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
isLenient = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.LocalTime
|
||||
|
||||
object LocalTimeSerializer: KSerializer<LocalTime> {
|
||||
override val descriptor = PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.LONG)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalTime) {
|
||||
// We use millis here for backwards compatibility in LocationSerializer
|
||||
encoder.encodeLong(value.toNanoOfDay() / 1000000L)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalTime {
|
||||
return LocalTime.ofNanoOfDay(decoder.decodeLong() * 1000000L)
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.search.location.OpeningSchedule
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.PolymorphicSerializer
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
|
||||
object OpeningScheduleSerializer : KSerializer<OpeningSchedule> {
|
||||
override val descriptor = PolymorphicSerializer(OpeningSchedule::class).descriptor
|
||||
|
||||
override fun serialize(encoder: Encoder, value: OpeningSchedule) {
|
||||
when (value) {
|
||||
is OpeningSchedule.TwentyFourSeven -> encoder.encodeString("twentyFourSeven")
|
||||
|
||||
is OpeningSchedule.Hours -> encoder.encodeSerializableValue(
|
||||
OpeningSchedule.Hours.serializer(),
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): OpeningSchedule {
|
||||
decoder as JsonDecoder
|
||||
val jsonElement = decoder.decodeJsonElement()
|
||||
|
||||
if ((jsonElement as? JsonPrimitive)?.content == "twentyFourSeven") {
|
||||
return OpeningSchedule.TwentyFourSeven
|
||||
}
|
||||
|
||||
if ("openingHours" in jsonElement.jsonObject.keys &&
|
||||
/* backwards compatibility */ !jsonElement.jsonObject["openingHours"]!!.jsonArray.isEmpty()
|
||||
) {
|
||||
return try {
|
||||
decoder.json.decodeFromJsonElement<OpeningSchedule.Hours>(
|
||||
jsonElement
|
||||
)
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("MM20", "Failed to deserialize OpeningSchedule.Hours", e)
|
||||
return OpeningSchedule.Hours(openingHours = emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
// fallback in case we receive data which was serialized before introducing OpeningScheduleSerializer.
|
||||
// here openingHours is empty, in which case it has to be 24/7, else it would indicate corrupted data.
|
||||
return OpeningSchedule.TwentyFourSeven
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
object UriSerializer : KSerializer<Uri> {
|
||||
override val descriptor = PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Uri) {
|
||||
// We use millis here for backwards compatibility in LocationSerializer
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Uri {
|
||||
return Uri.parse(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.mm20.launcher2.serialization
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
object ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
|
||||
override val descriptor = PrimitiveSerialDescriptor(javaClass.canonicalName!!, PrimitiveKind.LONG)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ZonedDateTime) {
|
||||
encoder.encodeLong(
|
||||
value.toEpochSecond()
|
||||
)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): ZonedDateTime {
|
||||
return ZonedDateTime.ofInstant(
|
||||
Instant.ofEpochSecond(decoder.decodeLong()),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
enum class WeatherIcon {
|
||||
Unknown,
|
||||
Clear,
|
||||
Cloudy,
|
||||
Cold,
|
||||
Drizzle,
|
||||
Haze,
|
||||
Fog,
|
||||
Hail,
|
||||
HeavyThunderstorm,
|
||||
HeavyThunderstormWithRain,
|
||||
Hot,
|
||||
MostlyCloudy,
|
||||
PartlyCloudy,
|
||||
Showers,
|
||||
Sleet,
|
||||
Snow,
|
||||
Storm,
|
||||
Thunderstorm,
|
||||
ThunderstormWithRain,
|
||||
Wind,
|
||||
BrokenClouds,
|
||||
}
|
||||
Reference in New Issue
Block a user