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
+3
View File
@@ -48,6 +48,9 @@ dependencies {
implementation(libs.koin.android)
implementation(libs.androidx.palette)
runtimeOnly(libs.androidx.compose.ui)
runtimeOnly(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.materialicons)
implementation(project(":core:ktx"))
implementation(project(":core:i18n"))
@@ -0,0 +1,538 @@
package de.mm20.launcher2.icons
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.materialIcon
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.group
val Icons.Rounded.Battery0Bar
get() = materialIcon("Icons.Rounded.Battery0Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(14f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery1Bar
get() = materialIcon("Icons.Rounded.Battery1Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(12f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery2Bar
get() = materialIcon("Icons.Rounded.Battery2Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(10f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery3Bar
get() = materialIcon("Icons.Rounded.Battery3Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(8f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery4Bar
get() = materialIcon("Icons.Rounded.Battery4Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(6f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery5Bar
get() = materialIcon("Icons.Rounded.Battery5Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(4f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.Battery6Bar
get() = materialIcon("Icons.Rounded.Battery6Bar") {
materialPath {
moveTo(17f, 5f)
verticalLineToRelative(16f)
curveToRelative(0f, 0.55f, -0.45f, 1f, -1f, 1f)
horizontalLineTo(8f)
curveToRelative(-0.55f, 0f, -1f, -0.45f, -1f, -1f)
verticalLineTo(5f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
verticalLineTo(3f)
curveToRelative(0f, -0.55f, 0.45f, -1f, 1f, -1f)
horizontalLineToRelative(2f)
curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f)
verticalLineToRelative(1f)
horizontalLineToRelative(2f)
curveTo(16.55f, 4f, 17f, 4.45f, 17f, 5f)
close()
moveTo(15f, 6f)
horizontalLineTo(9f)
verticalLineToRelative(2f)
horizontalLineToRelative(6f)
verticalLineTo(6f)
close()
}
}
val Icons.Rounded.BatteryCharging0Bar
get() = materialIcon("Icons.Rounded.BatteryCharging0Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 75.589844f)
lineTo(34.015625f, 75.589844f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(47.253906f, 32.126953f)
curveTo(46.605486f, 32.112843f, 45.950535f, 32.41843f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
lineTo(41.574219f, 54.802734f)
lineTo(41.574219f, 68.03125f)
curveTo(41.574219f, 69.996604f, 44.219867f, 70.640241f, 45.126953f, 68.939453f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.848696f, 48.201598f, 32.147575f, 47.253906f, 32.126953f)
close()
}
}
}
val Icons.Rounded.BatteryCharging1Bar
get() = materialIcon("Icons.Rounded.BatteryCharging1Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 68.03125f)
lineTo(45.611328f, 68.03125f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(34.015625f, 68.03125f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(41.574219f, 68.03125f)
lineTo(45.611328f, 68.03125f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
lineTo(41.574219f, 54.802734f)
lineTo(41.574219f, 68.03125f)
close()
}
}
}
val Icons.Rounded.BatteryCharging2Bar
get() = materialIcon("Icons.Rounded.BatteryCharging2Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 60.472656f)
lineTo(49.648438f, 60.472656f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 60.472656f)
lineTo(34.015625f, 60.472656f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(41.574219f, 60.472656f)
lineTo(49.648438f, 60.472656f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(34.809919f, 53.290899f, 35.716123f, 54.802734f, 37.152344f, 54.802734f)
lineTo(41.574219f, 54.802734f)
lineTo(41.574219f, 60.472656f)
close()
}
}
}
val Icons.Rounded.BatteryCharging3Bar
get() = materialIcon("Icons.Rounded.BatteryCharging3Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 52.914062f)
lineTo(53.683594f, 52.914062f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(36.048819f, 54.802734f, 35.258483f, 53.910469f, 35.259766f, 52.914062f)
lineTo(34.015625f, 52.914062f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(35.259766f, 52.914062f)
lineTo(53.683594f, 52.914062f)
lineTo(55.21875f, 50.041016f)
curveTo(55.899065f, 48.755976f, 54.955066f, 47.244141f, 53.556641f, 47.244141f)
lineTo(49.134766f, 47.244141f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(35.490234f, 52.005859f)
curveTo(35.332642f, 52.303535f, 35.260152f, 52.613661f, 35.259766f, 52.914062f)
close()
}
}
}
val Icons.Rounded.BatteryCharging4Bar
get() = materialIcon("Icons.Rounded.BatteryCharging4Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 45.353516f)
lineTo(49.134766f, 45.353516f)
lineTo(49.134766f, 47.244141f)
lineTo(53.556641f, 47.244141f)
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
lineTo(39.042969f, 45.353516f)
lineTo(34.015625f, 45.353516f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(39.042969f, 45.353516f)
lineTo(49.134766f, 45.353516f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(39.042969f, 45.353516f)
close()
}
}
}
val Icons.Rounded.BatteryCharging5Bar
get() = materialIcon("Icons.Rounded.BatteryCharging5Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 37.794922f)
lineTo(49.134766f, 37.794922f)
lineTo(49.134766f, 47.244141f)
lineTo(53.556641f, 47.244141f)
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
lineTo(43.080078f, 37.794922f)
lineTo(34.015625f, 37.794922f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(43.080078f, 37.794922f)
lineTo(49.134766f, 37.794922f)
lineTo(49.134766f, 34.015625f)
curveTo(49.134766f, 32.050271f, 46.489118f, 31.408588f, 45.582031f, 33.109375f)
lineTo(43.080078f, 37.794922f)
close()
}
}
}
val Icons.Rounded.BatteryCharging6Bar
get() = materialIcon("Icons.Rounded.BatteryCharging6Bar") {
group(
scaleX = 0.26458333f,
scaleY = 0.26458333f,
) {
materialPath {
moveTo(41.574219f, 7.5585938f)
curveTo(39.495479f, 7.5585937f, 37.794922f, 9.2591505f, 37.794922f, 11.337891f)
lineTo(37.794922f, 15.117188f)
lineTo(30.236328f, 15.117188f)
curveTo(28.157588f, 15.117187f, 26.457031f, 16.819697f, 26.457031f, 18.898438f)
lineTo(26.457031f, 79.369141f)
curveTo(26.457031f, 81.447881f, 28.157588f, 83.150391f, 30.236328f, 83.150391f)
lineTo(60.472656f, 83.150391f)
curveTo(62.551396f, 83.150391f, 64.251953f, 81.447881f, 64.251953f, 79.369141f)
lineTo(64.251953f, 18.898438f)
curveTo(64.251953f, 16.819697f, 62.551396f, 15.117188f, 60.472656f, 15.117188f)
lineTo(52.914062f, 15.117188f)
lineTo(52.914062f, 11.337891f)
curveTo(52.914063f, 9.2591505f, 51.213506f, 7.5585938f, 49.134766f, 7.5585938f)
lineTo(41.574219f, 7.5585938f)
close()
moveTo(34.015625f, 22.677734f)
lineTo(56.693359f, 22.677734f)
lineTo(56.693359f, 30.236328f)
lineTo(34.015625f, 30.236328f)
lineTo(34.015625f, 22.677734f)
close()
moveTo(47.253906f, 32.126953f)
curveTo(48.201598f, 32.147575f, 49.134766f, 32.848696f, 49.134766f, 34.015625f)
lineTo(49.134766f, 47.244141f)
lineTo(53.556641f, 47.244141f)
curveTo(54.955066f, 47.244141f, 55.899065f, 48.755976f, 55.21875f, 50.041016f)
lineTo(45.126953f, 68.939453f)
curveTo(44.219867f, 70.640241f, 41.574219f, 69.996604f, 41.574219f, 68.03125f)
lineTo(41.574219f, 54.802734f)
lineTo(37.152344f, 54.802734f)
curveTo(35.716123f, 54.802734f, 34.809919f, 53.290899f, 35.490234f, 52.005859f)
lineTo(45.582031f, 33.109375f)
curveTo(45.950535f, 32.41843f, 46.605486f, 32.112843f, 47.253906f, 32.126953f)
close()
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
package de.mm20.launcher2.icons
import android.graphics.drawable.Drawable
import androidx.compose.ui.graphics.vector.ImageVector
sealed interface LauncherIconLayer
@@ -53,4 +54,9 @@ data class TextLayer(
val color: Int = 0,
) : LauncherIconLayer
data class VectorLayer(
val vector: ImageVector,
val color: Int = 0,
) : LauncherIconLayer
object TransparentLayer: LauncherIconLayer
@@ -1,9 +1,11 @@
package de.mm20.launcher2.plugin
import android.content.ContentResolver
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.util.Log
import de.mm20.launcher2.plugin.config.SearchPluginConfig
import de.mm20.launcher2.plugin.config.QueryPluginConfig
import de.mm20.launcher2.plugin.config.WeatherPluginConfig
import de.mm20.launcher2.plugin.contracts.PluginContract
@@ -11,7 +13,25 @@ class PluginApi(
private val pluginAuthority: String,
private val contentResolver: ContentResolver,
) {
fun getSearchPluginConfig(): SearchPluginConfig? {
fun getConfig(): Bundle? {
val configBundle = try {
contentResolver.call(
Uri.Builder()
.scheme("content")
.authority(pluginAuthority)
.build(),
PluginContract.Methods.GetConfig,
null,
null
) ?: return null
} catch (e: Exception) {
Log.e("MM20", "Plugin $pluginAuthority threw exception", e)
return null
}
return configBundle
}
fun getSearchPluginConfig(): QueryPluginConfig? {
val configBundle = try {
contentResolver.call(
Uri.Builder()
@@ -27,7 +47,7 @@ class PluginApi(
return null
}
return SearchPluginConfig(configBundle)
return QueryPluginConfig(configBundle)
}
fun getWeatherPluginConfig(): WeatherPluginConfig? {
@@ -10,7 +10,7 @@ data class PluginPackage(
val author: String? = null,
val settings: Intent? = null,
val plugins: List<Plugin>,
val isOfficial: Boolean = false,
val isVerified: Boolean = false,
) {
val enabled: Boolean = plugins.all { it.enabled }
}
@@ -0,0 +1,190 @@
package de.mm20.launcher2.plugin
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.os.CancellationSignal
import android.util.Log
import de.mm20.launcher2.plugin.config.QueryPluginConfig
import de.mm20.launcher2.plugin.contracts.LocationPluginContract
import de.mm20.launcher2.plugin.contracts.PluginContract
import de.mm20.launcher2.plugin.contracts.SearchPluginContract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
private class NotUpdated : Exception("Not updated")
abstract class QueryPluginApi<TQuery, TResult>(
private val context: Context,
private val pluginAuthority: String,
) {
private fun getLanguage() = context.resources.configuration.locales[0].language
fun getConfig(): QueryPluginConfig? {
val configBundle = try {
context.contentResolver.call(
Uri.Builder()
.scheme("content")
.authority(pluginAuthority)
.build(),
PluginContract.Methods.GetConfig,
null,
null
) ?: return null
} catch (e: Exception) {
Log.e("MM20", "Plugin $pluginAuthority threw exception", e)
return null
}
return QueryPluginConfig(configBundle)
}
suspend fun search(query: TQuery, allowNetwork: Boolean): List<TResult> = withContext(Dispatchers.IO) {
val lang = getLanguage()
val uri = Uri.Builder()
.scheme("content")
.authority(pluginAuthority)
.path(LocationPluginContract.Paths.Search)
.appendQueryParameters(query)
.appendQueryParameter(
SearchPluginContract.Params.AllowNetwork,
allowNetwork.toString()
)
.appendQueryParameter(
SearchPluginContract.Params.Lang,
lang
)
.build()
val cancellationSignal = CancellationSignal()
return@withContext suspendCancellableCoroutine {
it.invokeOnCancellation {
cancellationSignal.cancel()
}
val cursor = try {
context.contentResolver.query(
uri,
null,
null,
cancellationSignal
)
} catch (e: Exception) {
Log.e("MM20", "Plugin $pluginAuthority threw exception", e)
it.resume(emptyList())
return@suspendCancellableCoroutine
}
if (cursor == null) {
Log.e("MM20", "Plugin $pluginAuthority returned null cursor")
it.resume(emptyList())
return@suspendCancellableCoroutine
}
val results = cursor.getData() ?: emptyList()
it.resume(results)
}
}
suspend fun get(id: String): Result<TResult?> = withContext(Dispatchers.IO) {
val uri = Uri.Builder()
.scheme("content")
.authority(pluginAuthority)
.path(SearchPluginContract.Paths.Root)
.appendPath(id)
.appendQueryParameter(SearchPluginContract.Params.Lang, getLanguage())
.build()
val cancellationSignal = CancellationSignal()
return@withContext suspendCancellableCoroutine {
it.invokeOnCancellation {
cancellationSignal.cancel()
}
val cursor = try {
context.contentResolver.query(
uri,
null,
null,
cancellationSignal
)
} catch (e: Exception) {
Log.e("MM20", "Plugin $pluginAuthority threw exception", e)
it.resume(Result.failure(e))
return@suspendCancellableCoroutine
}
if (cursor == null) {
Log.e("MM20", "Plugin $pluginAuthority returned null cursor")
it.resume(Result.success(null))
return@suspendCancellableCoroutine
}
val result = cursor.getData()?.firstOrNull()
if (result == null) {
it.resume(Result.success(null))
return@suspendCancellableCoroutine
}
it.resume(Result.success(result))
}
}
suspend fun refresh(item: TResult, lastUpdate: Long): Result<TResult?> = withContext(Dispatchers.IO) {
val uri = Uri.Builder()
.scheme("content")
.authority(pluginAuthority)
.path(SearchPluginContract.Paths.Refresh)
.appendQueryParameter(SearchPluginContract.Params.Lang, getLanguage())
.appendQueryParameter(SearchPluginContract.Params.UpdatedAt, lastUpdate.toString())
.build()
val cancellationSignal = CancellationSignal()
return@withContext suspendCancellableCoroutine {
it.invokeOnCancellation {
cancellationSignal.cancel()
}
val cursor = try {
context.contentResolver.query(
uri,
null,
item.toBundle(),
cancellationSignal
)
} catch (e: Exception) {
Log.e("MM20", "Plugin $pluginAuthority threw exception", e)
it.resume(Result.failure(e))
return@suspendCancellableCoroutine
}
if (cursor == null) {
Log.e("MM20", "Plugin $pluginAuthority returned null cursor")
it.resume(Result.failure(IllegalArgumentException()))
return@suspendCancellableCoroutine
}
if (cursor.extras?.getBoolean(SearchPluginContract.Extras.NotUpdated) == true) {
it.resume(Result.failure(NotUpdated()))
return@suspendCancellableCoroutine
}
val result = cursor.getData()?.firstOrNull()
if (result == null) {
it.resume(Result.success(null))
return@suspendCancellableCoroutine
}
it.resume(Result.success(result))
}
}
protected abstract fun Uri.Builder.appendQueryParameters(query: TQuery): Uri.Builder
protected abstract fun Cursor.getData(): List<TResult>?
protected abstract fun TResult.toBundle(): Bundle
}
@@ -2,8 +2,8 @@ package de.mm20.launcher2.plugin.config
import android.os.Bundle
fun SearchPluginConfig(bundle: Bundle): SearchPluginConfig? {
return SearchPluginConfig(
fun QueryPluginConfig(bundle: Bundle): QueryPluginConfig {
return QueryPluginConfig(
storageStrategy = valueOfOrElse(
bundle.getString(
"storageStrategy",
@@ -1,16 +1,135 @@
package de.mm20.launcher2.search
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.DirectionsBike
import androidx.compose.material.icons.rounded.AccountBalance
import androidx.compose.material.icons.rounded.AttachMoney
import androidx.compose.material.icons.rounded.Attractions
import androidx.compose.material.icons.rounded.BakeryDining
import androidx.compose.material.icons.rounded.Bed
import androidx.compose.material.icons.rounded.Bento
import androidx.compose.material.icons.rounded.Book
import androidx.compose.material.icons.rounded.BreakfastDining
import androidx.compose.material.icons.rounded.BrunchDining
import androidx.compose.material.icons.rounded.CarRental
import androidx.compose.material.icons.rounded.CarRepair
import androidx.compose.material.icons.rounded.Casino
import androidx.compose.material.icons.rounded.Checkroom
import androidx.compose.material.icons.rounded.Church
import androidx.compose.material.icons.rounded.ContentCut
import androidx.compose.material.icons.rounded.Diamond
import androidx.compose.material.icons.rounded.DirectionsBoat
import androidx.compose.material.icons.rounded.DirectionsBus
import androidx.compose.material.icons.rounded.DirectionsCar
import androidx.compose.material.icons.rounded.DirectionsTransit
import androidx.compose.material.icons.rounded.Discount
import androidx.compose.material.icons.rounded.DownhillSkiing
import androidx.compose.material.icons.rounded.ElectricScooter
import androidx.compose.material.icons.rounded.EvStation
import androidx.compose.material.icons.rounded.Fastfood
import androidx.compose.material.icons.rounded.Festival
import androidx.compose.material.icons.rounded.FitnessCenter
import androidx.compose.material.icons.rounded.Flight
import androidx.compose.material.icons.rounded.Forest
import androidx.compose.material.icons.rounded.Gavel
import androidx.compose.material.icons.rounded.Hiking
import androidx.compose.material.icons.rounded.Hotel
import androidx.compose.material.icons.rounded.Icecream
import androidx.compose.material.icons.rounded.Kayaking
import androidx.compose.material.icons.rounded.KebabDining
import androidx.compose.material.icons.rounded.Liquor
import androidx.compose.material.icons.rounded.LocalAtm
import androidx.compose.material.icons.rounded.LocalBar
import androidx.compose.material.icons.rounded.LocalCafe
import androidx.compose.material.icons.rounded.LocalCarWash
import androidx.compose.material.icons.rounded.LocalConvenienceStore
import androidx.compose.material.icons.rounded.LocalFireDepartment
import androidx.compose.material.icons.rounded.LocalFlorist
import androidx.compose.material.icons.rounded.LocalGasStation
import androidx.compose.material.icons.rounded.LocalGroceryStore
import androidx.compose.material.icons.rounded.LocalHospital
import androidx.compose.material.icons.rounded.LocalLaundryService
import androidx.compose.material.icons.rounded.LocalMall
import androidx.compose.material.icons.rounded.LocalParking
import androidx.compose.material.icons.rounded.LocalPharmacy
import androidx.compose.material.icons.rounded.LocalPizza
import androidx.compose.material.icons.rounded.LocalPolice
import androidx.compose.material.icons.rounded.LocalPostOffice
import androidx.compose.material.icons.rounded.LocalTaxi
import androidx.compose.material.icons.rounded.LunchDining
import androidx.compose.material.icons.rounded.Moped
import androidx.compose.material.icons.rounded.Mosque
import androidx.compose.material.icons.rounded.Motorcycle
import androidx.compose.material.icons.rounded.Museum
import androidx.compose.material.icons.rounded.MusicNote
import androidx.compose.material.icons.rounded.Newspaper
import androidx.compose.material.icons.rounded.Nightlife
import androidx.compose.material.icons.rounded.NordicWalking
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.Paragliding
import androidx.compose.material.icons.rounded.Park
import androidx.compose.material.icons.rounded.Pets
import androidx.compose.material.icons.rounded.Phone
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Pool
import androidx.compose.material.icons.rounded.RamenDining
import androidx.compose.material.icons.rounded.Restaurant
import androidx.compose.material.icons.rounded.School
import androidx.compose.material.icons.rounded.ShoppingBag
import androidx.compose.material.icons.rounded.Skateboarding
import androidx.compose.material.icons.rounded.Snowboarding
import androidx.compose.material.icons.rounded.SoupKitchen
import androidx.compose.material.icons.rounded.Sports
import androidx.compose.material.icons.rounded.SportsBar
import androidx.compose.material.icons.rounded.SportsBaseball
import androidx.compose.material.icons.rounded.SportsBasketball
import androidx.compose.material.icons.rounded.SportsCricket
import androidx.compose.material.icons.rounded.SportsFootball
import androidx.compose.material.icons.rounded.SportsGolf
import androidx.compose.material.icons.rounded.SportsGymnastics
import androidx.compose.material.icons.rounded.SportsHandball
import androidx.compose.material.icons.rounded.SportsHockey
import androidx.compose.material.icons.rounded.SportsMartialArts
import androidx.compose.material.icons.rounded.SportsMotorsports
import androidx.compose.material.icons.rounded.SportsRugby
import androidx.compose.material.icons.rounded.SportsSoccer
import androidx.compose.material.icons.rounded.SportsTennis
import androidx.compose.material.icons.rounded.SportsVolleyball
import androidx.compose.material.icons.rounded.Stadium
import androidx.compose.material.icons.rounded.Subway
import androidx.compose.material.icons.rounded.Surfing
import androidx.compose.material.icons.rounded.Synagogue
import androidx.compose.material.icons.rounded.TakeoutDining
import androidx.compose.material.icons.rounded.TempleBuddhist
import androidx.compose.material.icons.rounded.TempleHindu
import androidx.compose.material.icons.rounded.TheaterComedy
import androidx.compose.material.icons.rounded.Theaters
import androidx.compose.material.icons.rounded.Train
import androidx.compose.material.icons.rounded.Tram
import androidx.compose.material.icons.rounded.Wc
import androidx.core.content.ContextCompat
import de.mm20.launcher2.base.R
import de.mm20.launcher2.icons.CableCar
import de.mm20.launcher2.icons.Candle
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.Dentistry
import de.mm20.launcher2.icons.Eyeglasses
import de.mm20.launcher2.icons.Monument
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import kotlinx.collections.immutable.ImmutableList
import java.time.DayOfWeek
import java.time.Duration
import de.mm20.launcher2.icons.Stethoscope
import de.mm20.launcher2.icons.VectorLayer
import de.mm20.launcher2.ktx.tryStartActivity
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.OpeningHours
import de.mm20.launcher2.search.location.OpeningSchedule
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.temporal.TemporalAdjusters
import android.location.Location as AndroidLocation
@@ -20,128 +139,182 @@ interface Location : SavableSearchable {
val longitude: Double
val fixMeUrl: String?
val category: LocationCategory?
val icon: LocationIcon?
val category: String?
val address: Address?
val street: String?
val houseNumber: String?
val openingSchedule: OpeningSchedule?
val websiteUrl: String?
val phoneNumber: String?
val emailAddress: String?
val userRating: Float?
val userRatingCount: Int?
val openingSchedule: OpeningSchedule?
val departures: List<Departure>?
val attribution: Attribution?
get() = null
override val preferDetailsOverLaunch: Boolean
get() = true
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("geo:$latitude,$longitude?q=${Uri.encode(label)}")
),
options
)
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when (category) {
LocationCategory.FAST_FOOD, LocationCategory.RESTAURANT -> with(
labelOverride ?: label
) {
when {
contains(
"pizza",
ignoreCase = true
) -> R.drawable.ic_location_pizza to R.color.red
val (vector, bgColor) = when (icon) {
// blue: transportation
LocationIcon.Car -> Icons.Rounded.DirectionsCar to R.color.blue
LocationIcon.CarRental -> Icons.Rounded.CarRental to R.color.blue
LocationIcon.CarRepair -> Icons.Rounded.CarRepair to R.color.blue
LocationIcon.CarWash -> Icons.Rounded.LocalCarWash to R.color.blue
LocationIcon.ChargingStation -> Icons.Rounded.EvStation to R.color.blue
LocationIcon.GasStation -> Icons.Rounded.LocalGasStation to R.color.blue
LocationIcon.Parking -> Icons.Rounded.LocalParking to R.color.blue
LocationIcon.Bus -> Icons.Rounded.DirectionsBus to R.color.blue
LocationIcon.Tram -> Icons.Rounded.Tram to R.color.blue
LocationIcon.Subway -> Icons.Rounded.Subway to R.color.blue
LocationIcon.Train -> Icons.Rounded.Train to R.color.blue
LocationIcon.CableCar -> Icons.Rounded.CableCar to R.color.blue
LocationIcon.Airport -> Icons.Rounded.Flight to R.color.blue
LocationIcon.Boat -> Icons.Rounded.DirectionsBoat to R.color.blue
LocationIcon.Moped -> Icons.Rounded.Moped to R.color.blue
LocationIcon.Bike -> Icons.AutoMirrored.Rounded.DirectionsBike to R.color.blue
LocationIcon.Motorcycle -> Icons.Rounded.Motorcycle to R.color.blue
LocationIcon.ElectricScooter -> Icons.Rounded.ElectricScooter to R.color.blue
LocationIcon.Taxi -> Icons.Rounded.LocalTaxi to R.color.blue
LocationIcon.GenericTransit -> Icons.Rounded.DirectionsTransit to R.color.blue
contains(
"ramen",
ignoreCase = true
) -> R.drawable.ic_location_ramen to R.color.orange
// cyan: art, culture, entertainment
LocationIcon.ArtGallery -> Icons.Rounded.Palette to R.color.cyan
LocationIcon.Museum -> Icons.Rounded.Museum to R.color.cyan
LocationIcon.Theater -> Icons.Rounded.TheaterComedy to R.color.cyan
LocationIcon.MovieTheater -> Icons.Rounded.Theaters to R.color.cyan
LocationIcon.AmusementPark -> Icons.Rounded.Attractions to R.color.cyan
LocationIcon.NightClub -> Icons.Rounded.Nightlife to R.color.cyan
LocationIcon.ConcertHall -> Icons.Rounded.MusicNote to R.color.cyan
LocationIcon.Stadium -> Icons.Rounded.Stadium to R.color.cyan
LocationIcon.Casino -> Icons.Rounded.Casino to R.color.cyan
LocationIcon.Circus -> Icons.Rounded.Festival to R.color.cyan
// pink: hotels
LocationIcon.Hotel -> Icons.Rounded.Hotel to R.color.pink
// orange: food and drink
LocationIcon.Restaurant -> Icons.Rounded.Restaurant to R.color.orange
LocationIcon.Cafe -> Icons.Rounded.LocalCafe to R.color.orange
LocationIcon.FastFood -> Icons.Rounded.Fastfood to R.color.orange
LocationIcon.Pizza -> Icons.Rounded.LocalPizza to R.color.orange
LocationIcon.Burger -> Icons.Rounded.LunchDining to R.color.orange
LocationIcon.Kebab -> Icons.Rounded.KebabDining to R.color.orange
LocationIcon.IceCream -> Icons.Rounded.Icecream to R.color.orange
LocationIcon.Ramen -> Icons.Rounded.RamenDining to R.color.orange
LocationIcon.Soup -> Icons.Rounded.SoupKitchen to R.color.orange
LocationIcon.Bar -> Icons.Rounded.LocalBar to R.color.orange
LocationIcon.Brunch -> Icons.Rounded.BrunchDining to R.color.orange
LocationIcon.Breakfast -> Icons.Rounded.BreakfastDining to R.color.orange
LocationIcon.Pub -> Icons.Rounded.SportsBar to R.color.orange
LocationIcon.JapaneseCuisine -> Icons.Rounded.Bento to R.color.orange
LocationIcon.AsianCuisine -> Icons.Rounded.TakeoutDining to R.color.orange
// indigo: business and shopping
LocationIcon.Shopping -> Icons.Rounded.ShoppingBag to R.color.indigo
LocationIcon.Supermarket -> Icons.Rounded.LocalGroceryStore to R.color.indigo
LocationIcon.Florist -> Icons.Rounded.LocalFlorist to R.color.indigo
LocationIcon.Kiosk -> Icons.Rounded.Newspaper to R.color.indigo
LocationIcon.FurnitureStore -> Icons.Rounded.Bed to R.color.indigo
LocationIcon.CellPhoneStore -> Icons.Rounded.Phone to R.color.indigo
LocationIcon.BookStore -> Icons.Rounded.Book to R.color.indigo
LocationIcon.ClothingStore -> Icons.Rounded.Checkroom to R.color.indigo
LocationIcon.ConvenienceStore -> Icons.Rounded.LocalConvenienceStore to R.color.indigo
LocationIcon.DiscountStore -> Icons.Rounded.Discount to R.color.indigo
LocationIcon.JewelryStore -> Icons.Rounded.Diamond to R.color.indigo
LocationIcon.ShoppingMall -> Icons.Rounded.LocalMall to R.color.indigo
LocationIcon.LiquorStore -> Icons.Rounded.Liquor to R.color.indigo
LocationIcon.PetStore -> Icons.Rounded.Pets to R.color.indigo
LocationIcon.Bakery -> Icons.Rounded.BakeryDining to R.color.indigo
LocationIcon.Optician -> Icons.Rounded.Eyeglasses to R.color.indigo
LocationIcon.Pharmacy -> Icons.Rounded.LocalPharmacy to R.color.indigo
LocationIcon.HairSalon -> Icons.Rounded.ContentCut to R.color.indigo
LocationIcon.Laundromat -> Icons.Rounded.LocalLaundryService to R.color.indigo
// purple: sports and recreation
LocationIcon.Sports -> Icons.Rounded.Sports to R.color.purple
LocationIcon.FitnessCenter -> Icons.Rounded.FitnessCenter to R.color.purple
LocationIcon.Soccer -> Icons.Rounded.SportsSoccer to R.color.purple
LocationIcon.Basketball -> Icons.Rounded.SportsBasketball to R.color.purple
LocationIcon.Golf -> Icons.Rounded.SportsGolf to R.color.purple
LocationIcon.Tennis -> Icons.Rounded.SportsTennis to R.color.purple
LocationIcon.Baseball -> Icons.Rounded.SportsBaseball to R.color.purple
LocationIcon.Rugby -> Icons.Rounded.SportsRugby to R.color.purple
LocationIcon.AmericanFootball -> Icons.Rounded.SportsFootball to R.color.purple
LocationIcon.Hiking -> Icons.Rounded.Hiking to R.color.purple
LocationIcon.Swimming -> Icons.Rounded.Pool to R.color.purple
LocationIcon.Surfing -> Icons.Rounded.Surfing to R.color.purple
LocationIcon.Motorsports -> Icons.Rounded.SportsMotorsports to R.color.purple
LocationIcon.Handball -> Icons.Rounded.SportsHandball to R.color.purple
LocationIcon.Volleyball -> Icons.Rounded.SportsVolleyball to R.color.purple
LocationIcon.Skiing -> Icons.Rounded.DownhillSkiing to R.color.purple
LocationIcon.Kayaking -> Icons.Rounded.Kayaking to R.color.purple
LocationIcon.Skateboarding -> Icons.Rounded.Skateboarding to R.color.purple
LocationIcon.Cricket -> Icons.Rounded.SportsCricket to R.color.purple
LocationIcon.MartialArts -> Icons.Rounded.SportsMartialArts to R.color.purple
LocationIcon.NordicWalking -> Icons.Rounded.NordicWalking to R.color.purple
LocationIcon.Paragliding -> Icons.Rounded.Paragliding to R.color.purple
LocationIcon.Gymnastics -> Icons.Rounded.SportsGymnastics to R.color.purple
LocationIcon.Snowboarding -> Icons.Rounded.Snowboarding to R.color.purple
LocationIcon.Hockey -> Icons.Rounded.SportsHockey to R.color.purple
// green: finances
LocationIcon.Bank -> Icons.Rounded.AttachMoney to R.color.green
LocationIcon.Atm -> Icons.Rounded.LocalAtm to R.color.green
// red: health
LocationIcon.Hospital -> Icons.Rounded.LocalHospital to R.color.red
LocationIcon.Clinic -> Icons.Rounded.LocalHospital to R.color.red
LocationIcon.Dentist -> Icons.Rounded.Dentistry to R.color.red
LocationIcon.Physician -> Icons.Rounded.Stethoscope to R.color.red
// light green: nature
LocationIcon.Park -> Icons.Rounded.Park to R.color.lightgreen
LocationIcon.Forest -> Icons.Rounded.Forest to R.color.lightgreen
// brown: places of worship and remembrance
LocationIcon.Monument -> Icons.Rounded.Monument to R.color.brown
LocationIcon.Church -> Icons.Rounded.Church to R.color.brown
LocationIcon.Mosque -> Icons.Rounded.Mosque to R.color.brown
LocationIcon.Synagogue -> Icons.Rounded.Synagogue to R.color.brown
LocationIcon.HinduTemple -> Icons.Rounded.TempleHindu to R.color.brown
LocationIcon.BuddhistTemple -> Icons.Rounded.TempleBuddhist to R.color.brown
LocationIcon.Candle -> Icons.Rounded.Candle to R.color.brown
// bluegrey: public services
LocationIcon.GovernmentBuilding -> Icons.Rounded.AccountBalance to R.color.bluegrey
LocationIcon.Police -> Icons.Rounded.LocalPolice to R.color.bluegrey
LocationIcon.FireDepartment -> Icons.Rounded.LocalFireDepartment to R.color.bluegrey
LocationIcon.Courthouse -> Icons.Rounded.Gavel to R.color.bluegrey
LocationIcon.PostOffice -> Icons.Rounded.LocalPostOffice to R.color.bluegrey
LocationIcon.Library -> Icons.Rounded.Book to R.color.bluegrey
LocationIcon.School -> Icons.Rounded.School to R.color.bluegrey
LocationIcon.University -> Icons.Rounded.School to R.color.bluegrey
LocationIcon.PublicBathroom -> Icons.Rounded.Wc to R.color.bluegrey
contains(
"tapas",
ignoreCase = true
) -> R.drawable.ic_location_tapas to R.color.orange
contains(
"keba" /* b or p, depending on locale */,
ignoreCase = true
) -> R.drawable.ic_location_kebab to R.color.orange
category == LocationCategory.FAST_FOOD -> R.drawable.ic_location_fastfood to R.color.orange
else -> R.drawable.ic_location_restaurant to R.color.red
}
}
LocationCategory.BAR -> R.drawable.ic_location_bar to R.color.amber
LocationCategory.CAFE, LocationCategory.COFFEE_SHOP -> R.drawable.ic_location_cafe to R.color.brown
LocationCategory.HOTEL -> R.drawable.ic_location_hotel to R.color.green
LocationCategory.SUPERMARKET -> R.drawable.ic_location_supermarket to R.color.lightblue
LocationCategory.SCHOOL -> R.drawable.ic_location_school to R.color.purple
LocationCategory.PARKING -> R.drawable.ic_location_parking to R.color.blue
LocationCategory.FUEL -> R.drawable.ic_location_fuel to R.color.teal
LocationCategory.TOILETS -> R.drawable.ic_location_toilets to R.color.blue
LocationCategory.PHARMACY -> R.drawable.ic_location_pharmacy to R.color.pink
LocationCategory.HOSPITAL, LocationCategory.CLINIC -> R.drawable.ic_location_hospital to R.color.red
LocationCategory.POST_OFFICE -> R.drawable.ic_location_post_office to R.color.yellow
LocationCategory.PUB, LocationCategory.BIERGARTEN -> R.drawable.ic_location_pub to R.color.amber
LocationCategory.GRAVE_YARD -> R.drawable.ic_location_grave_yard to R.color.grey
LocationCategory.DOCTORS -> R.drawable.ic_location_doctors to R.color.red
LocationCategory.POLICE -> R.drawable.ic_location_police to R.color.blue
LocationCategory.DENTIST -> R.drawable.ic_location_dentist to R.color.lightblue
LocationCategory.LIBRARY, LocationCategory.BOOKS -> R.drawable.ic_location_library to R.color.brown
LocationCategory.COLLEGE, LocationCategory.UNIVERSITY -> R.drawable.ic_location_college to R.color.purple
LocationCategory.ICE_CREAM -> R.drawable.ic_location_ice_cream to R.color.pink
LocationCategory.THEATRE -> R.drawable.ic_location_theatre to R.color.purple
LocationCategory.PUBLIC_BUILDING -> R.drawable.ic_location_public_building to R.color.bluegrey
LocationCategory.CINEMA -> R.drawable.ic_location_cinema to R.color.purple
LocationCategory.NIGHTCLUB -> R.drawable.ic_location_nightclub to R.color.purple
LocationCategory.CONVENIENCE -> R.drawable.ic_location_convenience to R.color.lightblue
LocationCategory.CLOTHES -> R.drawable.ic_location_clothes to R.color.pink
LocationCategory.HAIRDRESSER, LocationCategory.BEAUTY -> R.drawable.ic_location_hairdresser to R.color.pink
LocationCategory.CAR_REPAIR -> R.drawable.ic_location_car_repair to R.color.blue
LocationCategory.BAKERY -> R.drawable.ic_location_bakery to R.color.brown
LocationCategory.CAR -> R.drawable.ic_location_car to R.color.blue
LocationCategory.MOBILE_PHONE -> R.drawable.ic_location_mobile_phone to R.color.blue
LocationCategory.FURNITURE -> R.drawable.ic_location_furniture to R.color.brown
LocationCategory.ALCOHOL -> R.drawable.ic_location_alcohol to R.color.amber
LocationCategory.FLORIST -> R.drawable.ic_location_florist to R.color.green
LocationCategory.HARDWARE -> R.drawable.ic_location_hardware to R.color.brown
LocationCategory.ELECTRONICS -> R.drawable.ic_location_electronics to R.color.blue
LocationCategory.SHOES -> R.drawable.ic_location_shoes to R.color.pink
LocationCategory.MALL, LocationCategory.DEPARTMENT_STORE, LocationCategory.CHEMIST -> R.drawable.ic_location_mall to R.color.blue
LocationCategory.OPTICIAN -> R.drawable.ic_location_optician to R.color.blue
LocationCategory.JEWELRY -> R.drawable.ic_location_jewelry to R.color.pink
LocationCategory.GIFT -> R.drawable.ic_location_gift to R.color.pink
LocationCategory.BICYCLE -> R.drawable.ic_location_bicycle to R.color.blue
LocationCategory.LAUNDRY -> R.drawable.ic_location_laundry to R.color.blue
LocationCategory.COMPUTER -> R.drawable.ic_location_computer to R.color.blue
LocationCategory.TOBACCO -> R.drawable.ic_location_tobacco to R.color.amber
LocationCategory.WINE -> R.drawable.ic_location_wine to R.color.amber
LocationCategory.PHOTO -> R.drawable.ic_location_photo to R.color.blue
LocationCategory.BANK -> R.drawable.ic_location_bank to R.color.blue
LocationCategory.SOCCER -> R.drawable.ic_location_soccer to R.color.green
LocationCategory.BASKETBALL -> R.drawable.ic_location_basketball to R.color.orange
LocationCategory.TENNIS -> R.drawable.ic_location_tennis to R.color.orange
LocationCategory.FITNESS, LocationCategory.FITNESS_CENTRE -> R.drawable.ic_location_fitness to R.color.orange
LocationCategory.TRAM_STOP -> R.drawable.ic_location_tram_stop to R.color.blue
LocationCategory.RAILWAY_STATION -> R.drawable.ic_location_railway_stop to R.color.lightblue
LocationCategory.BUS_STATION, LocationCategory.BUS_STOP -> R.drawable.ic_location_bus_station to R.color.blue
LocationCategory.ATM -> R.drawable.ic_location_atm to R.color.green
LocationCategory.ART -> R.drawable.ic_location_art to R.color.deeporange
LocationCategory.KIOSK -> R.drawable.ic_location_kiosk to R.color.bluegrey
LocationCategory.MUSEUM -> R.drawable.ic_location_museum to R.color.deeporange
LocationCategory.PARCEL_LOCKER -> R.drawable.ic_location_parcel_locker to R.color.bluegrey
LocationCategory.TRAVEL_AGENCY -> R.drawable.ic_location_travel_agency to R.color.lightblue
else -> R.drawable.ic_location_place to R.color.bluegrey
null -> Icons.Rounded.Place to R.color.bluegrey
}
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, resId)!!,
scale = 0.5f,
foregroundLayer = VectorLayer(
vector = vector,
color = ContextCompat.getColor(context, bgColor)
),
backgroundLayer = ColorLayer(ContextCompat.getColor(context, bgColor))
)
}
fun toAndroidLocation(): AndroidLocation {
val location = AndroidLocation("KvaesitsoLocationProvider")
location.latitude = latitude
location.longitude = longitude
return location
}
fun toAndroidLocation(): AndroidLocation =
AndroidLocation("KvaesitsoLocationProvider").apply {
this.latitude = this@Location.latitude
this.longitude = this@Location.longitude
}
fun distanceTo(androidLocation: AndroidLocation): Float {
return androidLocation.distanceTo(this.toAndroidLocation())
@@ -149,107 +322,25 @@ interface Location : SavableSearchable {
fun distanceTo(otherLocation: Location): Float =
this.distanceTo(otherLocation.toAndroidLocation())
fun distanceTo(
latitude: Double,
longitude: Double,
locationProvider: String = "KvaesitsoLocationProvider"
): Float =
this.distanceTo(AndroidLocation(locationProvider).apply {
this.latitude = latitude
this.longitude = longitude
})
}
// https://taginfo.openstreetmap.org/tags
// 'amenity', 'shop', 'sport' of which the most important
enum class LocationCategory {
RESTAURANT,
FAST_FOOD,
BAR,
CAFE,
HOTEL,
SUPERMARKET,
OTHER,
SCHOOL,
PARKING,
FUEL,
TOILETS,
PHARMACY,
HOSPITAL,
POST_OFFICE,
PUB,
GRAVE_YARD,
DOCTORS,
POLICE,
DENTIST,
LIBRARY,
COLLEGE,
ICE_CREAM,
THEATRE,
PUBLIC_BUILDING,
CINEMA,
NIGHTCLUB,
BIERGARTEN,
CLINIC,
UNIVERSITY,
DEPARTMENT_STORE,
CLOTHES,
CONVENIENCE,
HAIRDRESSER,
CAR_REPAIR,
BEAUTY,
BOOKS,
BAKERY,
CAR,
MOBILE_PHONE,
FURNITURE,
ALCOHOL,
FLORIST,
HARDWARE,
ELECTRONICS,
SHOES,
MALL,
OPTICIAN,
JEWELRY,
GIFT,
BICYCLE,
LAUNDRY,
COMPUTER,
TOBACCO,
WINE,
PHOTO,
COFFEE_SHOP,
BANK,
SOCCER,
BASKETBALL,
TENNIS,
FITNESS,
TRAM_STOP,
RAILWAY_STATION,
RAILWAY_STOP,
BUS_STATION,
ATM,
ART,
KIOSK,
BUS_STOP,
MUSEUM,
PARCEL_LOCKER,
CHEMIST,
TRAVEL_AGENCY,
FITNESS_CENTRE
fun OpeningSchedule.isOpen(date: LocalDateTime = LocalDateTime.now()): Boolean = when (this) {
is OpeningSchedule.TwentyFourSeven -> true
is OpeningSchedule.Hours -> openingHours.any { it.isOpen(date) }
}
data class OpeningHours(
val dayOfWeek: DayOfWeek,
val startTime: LocalTime,
val duration: Duration
) {
fun isOpen(date: LocalDateTime = LocalDateTime.now()): Boolean {
val startTime = date.with(TemporalAdjusters.previousOrSame(dayOfWeek)).with(startTime)
val endTime = startTime.plus(duration)
return date in startTime..<endTime
}
override fun toString(): String = "$dayOfWeek $startTime-${startTime.plus(duration)}"
}
data class OpeningSchedule(
val isTwentyFourSeven: Boolean,
val openingHours: ImmutableList<OpeningHours>
) {
fun isOpen(date: LocalDateTime = LocalDateTime.now()): Boolean {
return isTwentyFourSeven || openingHours.any { it.isOpen(date) }
}
fun OpeningHours.isOpen(date: LocalDateTime = LocalDateTime.now()): Boolean {
val startTime = date.with(TemporalAdjusters.previousOrSame(dayOfWeek)).with(startTime)
val endTime = startTime.plus(duration)
return date in startTime..<endTime
}
@@ -8,7 +8,7 @@ package de.mm20.launcher2.search
*/
interface UpdatableSearchable<T : SavableSearchable> {
val timestamp: Long
val updatedSelf: (suspend () -> UpdateResult<T>)?
val updatedSelf: (suspend (SavableSearchable) -> UpdateResult<T>)?
}
sealed class UpdateResult<out T> {
@@ -16,3 +16,16 @@ sealed class UpdateResult<out T> {
data class TemporarilyUnavailable<T>(val cause: Throwable? = null) : UpdateResult<T>()
data class PermanentlyUnavailable<T>(val cause: Throwable? = null) : UpdateResult<T>()
}
fun <T>Result<T?>.asUpdateResult(): UpdateResult<T> {
return if (isSuccess) {
val refreshed = getOrNull()
if (refreshed == null) {
UpdateResult.PermanentlyUnavailable()
} else {
UpdateResult.Success(refreshed)
}
} else {
UpdateResult.TemporarilyUnavailable(exceptionOrNull())
}
}
@@ -1,5 +1,7 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.search.location.OpeningHours
import de.mm20.launcher2.search.location.OpeningSchedule
import kotlinx.collections.immutable.persistentListOf
import org.junit.Assert
import org.junit.Test
@@ -10,7 +10,9 @@ import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.PI
import kotlinx.coroutines.channels.awaitClose
+1 -1
View File
@@ -757,7 +757,7 @@
<string name="preference_default_filter_summary">Dostosuj domyślny filtr wyszukiwania</string>
<string name="preference_filter_bar">Pokaż pasek filtrów</string>
<string name="preference_filter_bar_summary">Pokaż szybkie filtry nad klawiaturą</string>
<string name="location_closes">Zamyka się za</string>
<string name="location_closes">Zamyka się za %1$s</string>
<string name="location_closes_other_day">Zamknięcie %1$s, %2$s</string>
<string name="location_opens">Otwórz o %1$s</string>
<string name="location_opens_other_day">Otwórz %1$s, %2$s</string>
+49
View File
@@ -925,6 +925,7 @@
<string name="search_filter_tools">Tools</string>
<string name="search_filter_online">Online results</string>
<string name="search_filter_apps">Apps</string>
<string name="plugin_type_locationsearch">Location search</string>
<string name="preference_default_filter">Default filter</string>
<string name="preference_default_filter_summary">Customize the default filter for searches</string>
<string name="preference_filter_bar">Show filter bar</string>
@@ -932,6 +933,8 @@
<string name="preference_customize_filter_bar">Customize filter bar</string>
<string name="preference_customize_filter_bar_summary">Customize which items are included in the filter bar</string>
<string name="filter_settings_network_warning">The current filter enables online results by default. Your search queries might unintentionally be sent to external web services. For privacy reasons, this is not recommended.</string>
<string name="preference_search_osm_locations">OpenStreetMap</string>
<string name="preference_search_osm_locations_summary">Search OpenStreetMap for shops and other places in the local area</string>
<string name="poi_category_restaurant">Restaurant</string>
<string name="poi_category_fast_food">Fast food</string>
<string name="poi_category_bar">Bar</string>
@@ -1006,4 +1009,50 @@
<string name="poi_category_chemist">Drug store</string>
<string name="poi_category_travel_agency">Travel agency</string>
<string name="poi_category_fitness_center">Fitness center</string>
<string name="poi_category_church">Church</string>
<string name="poi_category_mosque">Mosque</string>
<string name="poi_category_buddhist_temple">Buddhist temple</string>
<string name="poi_category_hindu_temple">Hindu temple</string>
<string name="poi_category_synagogue">Synagogue</string>
<string name="poi_category_pizza_restaurant">Pizza restaurant</string>
<string name="poi_category_burger_restaurant">Burger restaurant</string>
<string name="poi_category_place_of_worship">Place of worship</string>
<string name="poi_category_chinese_restaurant">Chinese restaurant</string>
<string name="poi_category_japanese_restaurant">Japanese restaurant</string>
<string name="poi_category_kebab_restaurant">Kebab restaurant</string>
<string name="poi_category_asian_restaurant">Asian restaurant</string>
<string name="poi_category_ramen_restaurant">Ramen restaurant</string>
<string name="poi_category_soup_restaurant">Soup restaurant</string>
<string name="poi_category_brunch_restaurant">Brunch restaurant</string>
<string name="poi_category_breakfast_restaurant">Breakfast restaurant</string>
<string name="poi_category_car_wash">Car wash</string>
<string name="poi_category_charging_station">Charging station</string>
<string name="poi_category_motorcycle_rental">Motorcycle rental</string>
<string name="poi_category_gallery">Gallery</string>
<string name="poi_category_amusement_park">Amusement park</string>
<string name="poi_category_concert_hall">Concert hall</string>
<string name="poi_category_stadium">Stadium</string>
<string name="poi_category_casino">Casino</string>
<string name="poi_category_shopping_center">Shopping center</string>
<string name="poi_category_discount_store">Discount store</string>
<string name="poi_category_pet">Pet store</string>
<string name="poi_category_shopping">Shopping</string>
<string name="poi_category_swimming">Swimming pool</string>
<string name="poi_category_martial_arts">Martial arts</string>
<string name="poi_category_golf">Golf</string>
<string name="poi_category_gymnastics">Gymnastics</string>
<string name="poi_category_ice_hockey">Ice hockey</string>
<string name="poi_category_baseball">Baseball</string>
<string name="poi_category_american_football">American football</string>
<string name="poi_category_handball">Handball</string>
<string name="poi_category_volleyball">Volleyball</string>
<string name="poi_category_skiing">Skiing</string>
<string name="poi_category_cricket">Cricket</string>
<string name="poi_category_park">Park</string>
<string name="poi_category_monument">Monument</string>
<string name="poi_category_government_building">Government building</string>
<string name="poi_category_fire_station">Fire station</string>
<string name="poi_category_courthouse">Courthouse</string>
<string name="poi_category_townhall">Townhall</string>
<string name="preference_search_locations_radius_large_radius_warning">Large search radii can significantly slow down location search.</string>
</resources>
@@ -2,9 +2,8 @@ package de.mm20.launcher2.ktx
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString
inline fun <reified T>Json.decodeFromStringOrNull(json: String?): T? {
inline fun <reified T> Json.decodeFromStringOrNull(json: String?): T? {
if (json == null) return null
return try {
decodeFromString(json)
@@ -2,19 +2,17 @@ package de.mm20.launcher2.ktx
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.Context
fun PendingIntent.sendWithBackgroundPermission() {
fun PendingIntent.sendWithBackgroundPermission(context: Context) {
if (isAtLeastApiLevel(34)) {
val options = ActivityOptions.makeBasic()
.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
)
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
)
.toBundle()
send(options)
send(context, 0, null, null, null, null, options)
} else {
send()
send(context, 0, null)
}
}
@@ -3,6 +3,7 @@ package de.mm20.launcher2.preferences
import android.content.Context
import de.mm20.launcher2.preferences.migrations.Migration1
import de.mm20.launcher2.preferences.migrations.Migration2
import de.mm20.launcher2.preferences.migrations.Migration3
import de.mm20.launcher2.settings.BaseSettings
internal class LauncherDataStore(
@@ -15,6 +16,7 @@ internal class LauncherDataStore(
migrations = listOf(
Migration1(legacyDataStore),
Migration2(),
Migration3(),
),
) {
@@ -135,7 +135,9 @@ data class LauncherSettingsData internal constructor(
val weatherProviderSettings: Map<String, ProviderSettings> = emptyMap(),
val weatherImperialUnits: Boolean = false,
@Deprecated("Use locationSearchProviders instead")
val locationSearchEnabled: Boolean = false,
val locationSearchProviders: Set<String> = setOf("openstreetmaps"),
val locationSearchImperialUnits: Boolean = false,
val locationSearchRadius: Int = 1500,
val locationSearchHideUncategorized: Boolean = true,
@@ -0,0 +1,24 @@
package de.mm20.launcher2.preferences.migrations
import androidx.datastore.core.DataMigration
import de.mm20.launcher2.preferences.LauncherSettingsData
class Migration3: DataMigration<LauncherSettingsData> {
override suspend fun cleanUp() {
}
override suspend fun shouldMigrate(currentData: LauncherSettingsData): Boolean {
return currentData.schemaVersion < 3
}
override suspend fun migrate(currentData: LauncherSettingsData): LauncherSettingsData {
return currentData.copy(
schemaVersion = 3,
locationSearchProviders = buildSet {
if (currentData.locationSearchEnabled) {
add("openstreetmaps")
}
}
)
}
}
@@ -1,16 +1,16 @@
package de.mm20.launcher2.preferences.search
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class LocationSearchSettings internal constructor(
private val launcherDataStore: LauncherDataStore,
) {
val data
get() = launcherDataStore.data.map {
LocationSearchSettingsData(
enabled = it.locationSearchEnabled,
providers = it.locationSearchProviders,
searchRadius = it.locationSearchRadius,
hideUncategorized = it.locationSearchHideUncategorized,
overpassUrl = it.locationSearchOverpassUrl,
@@ -22,12 +22,32 @@ class LocationSearchSettings internal constructor(
)
}
val enabled
get() = launcherDataStore.data.map { it.locationSearchEnabled }
val enabledProviders: Flow<Set<String>>
get() = launcherDataStore.data.map { it.locationSearchProviders }
fun setEnabled(enabled: Boolean) {
val osmLocations
get() = launcherDataStore.data.map { it.locationSearchProviders.contains("openstreetmaps") }
fun setOsmLocations(osmLocations: Boolean) {
launcherDataStore.update {
it.copy(locationSearchEnabled = enabled)
if (osmLocations) {
it.copy(locationSearchProviders = it.locationSearchProviders + "openstreetmaps")
} else {
it.copy(locationSearchProviders = it.locationSearchProviders - "openstreetmaps")
}
}
}
val enabledPlugins: Flow<Set<String>>
get() = launcherDataStore.data.map { it.locationSearchProviders - "openstreetmaps" }
fun setPluginEnabled(authority: String, enabled: Boolean) {
launcherDataStore.update {
if (enabled) {
it.copy(locationSearchProviders = it.locationSearchProviders + authority)
} else {
it.copy(locationSearchProviders = it.locationSearchProviders - authority)
}
}
}
@@ -76,15 +96,6 @@ class LocationSearchSettings internal constructor(
}
}
val showPositionOnMap
get() = launcherDataStore.data.map { it.locationSearchShowPositionOnMap }
fun setShowPositionOnMap(showPositionOnMap: Boolean) {
launcherDataStore.update {
it.copy(locationSearchShowPositionOnMap = showPositionOnMap)
}
}
val themeMap
get() = launcherDataStore.data.map { it.locationSearchThemeMap }
@@ -111,7 +122,7 @@ class LocationSearchSettings internal constructor(
}
data class LocationSearchSettingsData(
val enabled: Boolean = false,
val providers: Set<String> = setOf("openstreetmaps"),
val searchRadius: Int = 1500,
val hideUncategorized: Boolean = true,
val overpassUrl: String = LocationSearchSettings.DefaultOverpassUrl,
+16 -6
View File
@@ -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,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
}
}
}
@@ -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")
}
}
@@ -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")
}
}
@@ -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"
}
}
@@ -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)
}
}
@@ -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,
}