...
This commit is contained in:
parent
81e4c77644
commit
b8b088c1ab
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@ -73,5 +73,6 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
@ -39,6 +39,8 @@
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
android:stateNotNeeded="true"
|
||||
android:theme="@style/LauncherTheme"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:configChanges="keyboard|keyboardHidden|screenLayout|layoutDirection|navigation|orientation|uiMode"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import com.android.launcher3.GestureNavContract
|
||||
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
|
||||
|
||||
@ -19,6 +21,12 @@ class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
Log.e("LauncherActivity","newConfig.keyboard ${newConfig.keyboard}")
|
||||
Log.e("LauncherActivity","newConfig.keyboardHidden ${newConfig.keyboardHidden}")
|
||||
Log.e("LauncherActivity","newConfig.hardKeyboardHidden ${newConfig.hardKeyboardHidden}")
|
||||
}
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
enterHomeTransitionManager.clear()
|
||||
|
||||
@ -250,10 +250,10 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
previousResults = SearchResults(apps = apps)
|
||||
|
||||
searchActionResults.clear()
|
||||
appResults.mergeWith(apps)
|
||||
workAppResults.mergeWith(workApps)
|
||||
privateSpaceAppResults.mergeWith(privateApps)
|
||||
hiddenResults.mergeWith(hiddenItems)
|
||||
appResults.updateItems(apps)
|
||||
workAppResults.updateItems(workApps)
|
||||
privateSpaceAppResults.updateItems(privateApps)
|
||||
hiddenResults.updateItems(hiddenItems)
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -273,19 +273,31 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
workAppResults.clear()
|
||||
privateSpaceAppResults.clear()
|
||||
|
||||
appResults.mergeWith(results.apps, hiddenKeys, query)
|
||||
appShortcutResults.mergeWith(results.shortcuts, hiddenKeys, query)
|
||||
fileResults.mergeWith(results.files, hiddenKeys, query)
|
||||
appResults.updateItems(
|
||||
results.apps
|
||||
?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
appShortcutResults.updateItems(
|
||||
results.shortcuts
|
||||
?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
fileResults.updateItems(
|
||||
results.files
|
||||
?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
|
||||
contactResults.mergeWith(
|
||||
contactResults.updateItems(
|
||||
results.contacts?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
calendarResults.mergeWith(
|
||||
calendarResults.updateItems(
|
||||
results.calendars?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
locationResults.mergeWith(
|
||||
locationResults.updateItems(
|
||||
results.locations?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.let { locations ->
|
||||
devicePoseProvider.lastCachedLocation?.let {
|
||||
@ -298,17 +310,17 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
} ?: locations.applyRanking(query)
|
||||
}
|
||||
)
|
||||
articleResults.mergeWith(
|
||||
articleResults.updateItems(
|
||||
results.wikipedia?.applyRanking(query)
|
||||
)
|
||||
websiteResults.mergeWith(
|
||||
websiteResults.updateItems(
|
||||
results.websites?.applyRanking(query)
|
||||
)
|
||||
calculatorResults.mergeWith(results.calculators)
|
||||
unitConverterResults.mergeWith(results.unitConverters)
|
||||
calculatorResults.updateItems(results.calculators)
|
||||
unitConverterResults.updateItems(results.unitConverters)
|
||||
|
||||
if (results.searchActions != null) {
|
||||
searchActionResults.mergeWith(results.searchActions!!)
|
||||
searchActionResults.updateItems(results.searchActions!!)
|
||||
}
|
||||
|
||||
if (launchOnEnter.value) {
|
||||
@ -429,24 +441,18 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
return sorted.distinctBy { it.key }.toList()
|
||||
}
|
||||
|
||||
private fun <T> SnapshotStateList<T>.mergeWith(newItems: List<T>?) {
|
||||
val items = newItems ?: emptyList()
|
||||
val diff = toSet() subtract items.toSet()
|
||||
removeAll(diff)
|
||||
for ((i, item) in items.withIndex()) {
|
||||
if (i < size)
|
||||
set(i, item)
|
||||
else
|
||||
add(item)
|
||||
}
|
||||
/**
|
||||
* Merges a list of new items into the current SnapshotStateList.
|
||||
* It removes items that are in the current list but not in the new list.
|
||||
* Then, it updates existing items or adds new items from the new list.
|
||||
*
|
||||
* @param T The type of items in the list.
|
||||
* @param newItems The list of new items to merge with. If null, an empty list is used.
|
||||
*/
|
||||
private fun <T> SnapshotStateList<T>.updateItems(newItems: List<T>?) {
|
||||
clear()
|
||||
addAll(newItems ?: emptyList())
|
||||
}
|
||||
|
||||
private suspend fun <T : SavableSearchable> SnapshotStateList<T>.mergeWith(
|
||||
newItems: List<T>?,
|
||||
hiddenKeys: List<String>,
|
||||
query: String
|
||||
) = this.mergeWith((newItems ?: emptyList()).filterNot { hiddenKeys.contains(it.key) }
|
||||
.applyRanking(query))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -226,11 +226,19 @@ fun LocationItem(
|
||||
this@AnimatedContent
|
||||
)
|
||||
)
|
||||
|
||||
// val rateing =
|
||||
val formattedDistance =
|
||||
distance?.metersToLocalizedString(context, imperialUnits)
|
||||
val sublabel = listOf(location.category, formattedDistance)
|
||||
.fastFilterNotNull()
|
||||
.joinToString(" • ")
|
||||
val sublabel = if (location.category?.contains("|") == true) {
|
||||
listOf(location.category!!.split("|")!!.first(), formattedDistance,location.userRating)
|
||||
.fastFilterNotNull()
|
||||
.joinToString(" • ")
|
||||
} else {
|
||||
listOf(location.category, formattedDistance,location.userRating)
|
||||
.fastFilterNotNull()
|
||||
.joinToString(" • ")
|
||||
}
|
||||
val isOpenString = location.openingSchedule?.isOpen()
|
||||
?.let { stringResource(if (it) R.string.location_open else R.string.location_closed) }
|
||||
|
||||
|
||||
@ -66,7 +66,7 @@ fun WidgetColumn(
|
||||
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
modifier = modifier.fillMaxWidth()
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Column {
|
||||
|
||||
@ -693,4 +693,4 @@ fun ConfigureClockWidgetSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -442,7 +442,7 @@
|
||||
<string name="poi_category_books">서점</string>
|
||||
<string name="poi_category_florist">꽃집</string>
|
||||
<string name="poi_category_church">교회</string>
|
||||
<string name="poi_category_restaurant">레스토랑</string>
|
||||
<string name="poi_category_restaurant">레스토랑|음식점|식당|밥집|분식</string>
|
||||
<string name="poi_category_fast_food">패스트푸드</string>
|
||||
<string name="poi_category_hotel">호텔</string>
|
||||
<string name="poi_category_cafe">카페</string>
|
||||
|
||||
@ -249,7 +249,7 @@ internal class AppRepositoryImpl(
|
||||
appResults.addAll(apps.mapNotNull {
|
||||
val score = ResultScore(
|
||||
query = query,
|
||||
primaryFields = listOf(it.label),
|
||||
primaryFields = listOf(it.label,it.componentName.packageName),
|
||||
)
|
||||
if (score.score < 0.8f) return@mapNotNull null
|
||||
it.copy(
|
||||
|
||||
@ -0,0 +1,579 @@
|
||||
package de.mm20.launcher2.locations.providers.googleapi
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.ktx.ifNullOrEmpty
|
||||
import de.mm20.launcher2.ktx.into
|
||||
import de.mm20.launcher2.ktx.map
|
||||
import de.mm20.launcher2.ktx.stripStartOrNull
|
||||
import de.mm20.launcher2.locations.OsmLocationSerializer
|
||||
import de.mm20.launcher2.openstreetmaps.R
|
||||
import de.mm20.launcher2.search.Location
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchableSerializer
|
||||
import de.mm20.launcher2.search.UpdatableSearchable
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import de.mm20.launcher2.search.location.Address
|
||||
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 de.mm20.launcher2.search.location.PaymentMethod
|
||||
import de.westnordost.osm_opening_hours.model.ClockTime
|
||||
import de.westnordost.osm_opening_hours.model.ExtendedClockTime
|
||||
import de.westnordost.osm_opening_hours.model.LastNth
|
||||
import de.westnordost.osm_opening_hours.model.MonthRange
|
||||
import de.westnordost.osm_opening_hours.model.Nth
|
||||
import de.westnordost.osm_opening_hours.model.NthRange
|
||||
import de.westnordost.osm_opening_hours.model.Range
|
||||
import de.westnordost.osm_opening_hours.model.SingleMonth
|
||||
import de.westnordost.osm_opening_hours.model.SpecificWeekdays
|
||||
import de.westnordost.osm_opening_hours.model.StartingAtYear
|
||||
import de.westnordost.osm_opening_hours.model.TimeSpan
|
||||
import de.westnordost.osm_opening_hours.model.TimesSelector
|
||||
import de.westnordost.osm_opening_hours.model.TwentyFourSeven
|
||||
import de.westnordost.osm_opening_hours.model.Weekday
|
||||
import de.westnordost.osm_opening_hours.model.WeekdayRange
|
||||
import de.westnordost.osm_opening_hours.model.WeekdaysSelector
|
||||
import de.westnordost.osm_opening_hours.model.Year
|
||||
import de.westnordost.osm_opening_hours.model.YearRange
|
||||
import de.westnordost.osm_opening_hours.parser.toOpeningHoursOrNull
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import org.woheller69.AndroidAddressFormatter.OsmAddressFormatter
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
internal data class OsmLocation(
|
||||
internal val id: Long,
|
||||
override val label: String,
|
||||
override val icon: LocationIcon?,
|
||||
override val category: String?,
|
||||
override val latitude: Double,
|
||||
override val longitude: Double,
|
||||
override val address: Address?,
|
||||
override val openingSchedule: OpeningSchedule?,
|
||||
override val websiteUrl: String?,
|
||||
override val phoneNumber: String?,
|
||||
override val emailAddress: String? = null,
|
||||
override val labelOverride: String? = null,
|
||||
override val timestamp: Long,
|
||||
override var updatedSelf: (suspend (SavableSearchable) -> UpdateResult<Location>)? = null,
|
||||
override val userRating: Float?,
|
||||
override val acceptedPaymentMethods: Map<PaymentMethod, Boolean>?
|
||||
) : Location, UpdatableSearchable<Location> {
|
||||
|
||||
override val domain: String
|
||||
get() = DOMAIN
|
||||
override val key: String = "$domain://$id"
|
||||
override val fixMeUrl: String
|
||||
get() = FIXMEURL
|
||||
|
||||
override val userRatingCount: Int? = null
|
||||
override val departures: List<Departure>? = null
|
||||
|
||||
override fun overrideLabel(label: String): OsmLocation {
|
||||
return this.copy(labelOverride = label)
|
||||
}
|
||||
|
||||
override fun getSerializer(): SearchableSerializer {
|
||||
return OsmLocationSerializer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
internal const val DOMAIN = "osm"
|
||||
internal const val FIXMEURL = "https://www.openstreetmap.org/fixthemap"
|
||||
|
||||
internal val addressFormatter =
|
||||
OsmAddressFormatter(
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
|
||||
fun fromOverpassResponse(
|
||||
result: OverpassResponse,
|
||||
context: Context
|
||||
): List<OsmLocation> = result.elements.mapNotNull {
|
||||
it.tags ?: return@mapNotNull null
|
||||
val (category, icon) = it.tags.categorize(context)
|
||||
icon ?: return@mapNotNull null
|
||||
OsmLocation(
|
||||
id = it.id,
|
||||
label = it.tags["name"] ?: it.tags["brand"] ?: return@mapNotNull null,
|
||||
icon = icon,
|
||||
category = category,
|
||||
latitude = it.lat ?: it.center?.lat ?: return@mapNotNull null,
|
||||
longitude = it.lon ?: it.center?.lon ?: return@mapNotNull null,
|
||||
address = it.tags.toAddress(),
|
||||
openingSchedule = it.tags["opening_hours"]?.let { ot -> parseOpeningSchedule(ot) },
|
||||
websiteUrl = it.tags["website"] ?: it.tags["contact:website"],
|
||||
phoneNumber = it.tags["phone"] ?: it.tags["contact:phone"],
|
||||
emailAddress = it.tags["email"] ?: it.tags["contact:email"],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
userRating = it.tags["stars"]?.runCatching { this.toInt() }?.getOrNull()
|
||||
?.let { min(it, 5) / 5.0f },
|
||||
acceptedPaymentMethods = with(
|
||||
it.tags.mapNotNull { (key, value) ->
|
||||
(key.stripStartOrNull("payment:") ?: return@mapNotNull null) to value
|
||||
}.toMap()
|
||||
) {
|
||||
// best-effort way to take any method payment as it being available,
|
||||
// otherwise as being unavailable, or undefined
|
||||
mapOf(
|
||||
PaymentMethod.Card to listOf("credit_cards", "debit_cards", "cards"),
|
||||
PaymentMethod.Cash to listOf("cash")
|
||||
).mapNotNull { (method, values) ->
|
||||
when {
|
||||
values.any { this[it] in listOf("yes", "only") } -> method to true
|
||||
values.any { this[it] == "no" } -> method to false
|
||||
else -> null
|
||||
}
|
||||
}.toMap().takeUnless { it.isEmpty() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<String, String>.firstOfAlso(vararg strs: String, also: (String) -> Unit): String? {
|
||||
for (str in strs) {
|
||||
if (str in this) {
|
||||
also(str)
|
||||
return this[str]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun Map<String, String>.toAddress(): Address? {
|
||||
val formatAddrKeys = this.keys.filter { it.contains("addr") }.toMutableSet()
|
||||
if (formatAddrKeys.isEmpty()) return null
|
||||
|
||||
val addr = Address(
|
||||
city = firstOfAlso("addr:city", "addr:suburb", "addr:hamlet") { formatAddrKeys.remove(it) },
|
||||
state = firstOfAlso("addr:state", "addr:province") { formatAddrKeys.remove(it) },
|
||||
postalCode = firstOfAlso("addr:postcode") { formatAddrKeys.remove(it) },
|
||||
country = firstOfAlso("addr:country") { formatAddrKeys.remove(it) },
|
||||
)
|
||||
|
||||
val formattedRest = buildJsonObject {
|
||||
formatAddrKeys.mapNotNull {
|
||||
val (_, subkey) = it.split(':', limit = 2).takeIf { it.size == 2 }
|
||||
?: return@mapNotNull null
|
||||
put(subkey, this@toAddress[it])
|
||||
}
|
||||
}.takeIf { it.isNotEmpty() }?.toString()?.runCatching {
|
||||
OsmLocation.addressFormatter.format(
|
||||
this,
|
||||
this@toAddress["addr:country"] ?: Locale.getDefault().country
|
||||
)
|
||||
}?.getOrNull() ?: return addr
|
||||
|
||||
val lines = formattedRest.lines().filter { it.isNotBlank() }
|
||||
return addr.copy(
|
||||
address = lines.getOrNull(0),
|
||||
address2 = lines.getOrNull(1),
|
||||
address3 = lines.getOrNull(2),
|
||||
)
|
||||
}
|
||||
|
||||
private class MatchAnyReceiverScope<T, A, B> {
|
||||
private val pairs = mutableMapOf<T, Pair<A, B>>()
|
||||
operator fun get(key: T): Pair<A, B>? = pairs[key]
|
||||
infix fun T.with(pair: Pair<A, B>) = pairs.put(this, pair)
|
||||
}
|
||||
|
||||
private fun <A, B> Map<String, String>.matchAnyTag(
|
||||
key: String,
|
||||
block: MatchAnyReceiverScope<String, A, B>.() -> Unit
|
||||
): Pair<A, B>? {
|
||||
val scope = MatchAnyReceiverScope<String, A, B>()
|
||||
scope.block()
|
||||
return this[key]?.split(' ', ',', '.', ';')?.map { it.trim() }
|
||||
?.firstNotNullOfOrNull { scope[it] }
|
||||
}
|
||||
|
||||
private fun Map<String, String>.categorize(context: Context): Pair<String?, LocationIcon?> {
|
||||
val category = this.firstNotNullOfOrNull { (tag, value) ->
|
||||
val values = value.split(' ', ',', '.', ';').map { it.trim() }.toSet()
|
||||
when (tag.lowercase()) {
|
||||
|
||||
"shop" -> values.firstNotNullOfOrNull {
|
||||
when (it) {
|
||||
"florist" -> R.string.poi_category_florist to LocationIcon.Florist
|
||||
"kiosk" -> R.string.poi_category_kiosk to LocationIcon.Kiosk
|
||||
"furniture" -> R.string.poi_category_furniture to LocationIcon.FurnitureStore
|
||||
"cell_phones", "mobile_phone" -> R.string.poi_category_mobile_phone to LocationIcon.CellPhoneStore
|
||||
"books" -> R.string.poi_category_books to LocationIcon.BookStore
|
||||
"clothes" -> R.string.poi_category_clothes to LocationIcon.ClothingStore
|
||||
"convenience" -> R.string.poi_category_convenience to LocationIcon.ConvenienceStore
|
||||
"discount" -> R.string.poi_category_discount_store to LocationIcon.DiscountStore
|
||||
"jewelry" -> R.string.poi_category_jewelry to LocationIcon.JewelryStore
|
||||
"alcohol" -> R.string.poi_category_alcohol to LocationIcon.LiquorStore
|
||||
"pet", "pet_grooming" -> R.string.poi_category_pet to LocationIcon.PetStore
|
||||
"mall", "shopping_centre", "department_store" -> R.string.poi_category_mall to LocationIcon.ShoppingMall
|
||||
"supermarket" -> R.string.poi_category_supermarket to LocationIcon.Supermarket
|
||||
"bakery" -> R.string.poi_category_bakery to LocationIcon.Bakery
|
||||
"optician" -> R.string.poi_category_optician to LocationIcon.Optician
|
||||
"hairdresser" -> R.string.poi_category_hairdresser to LocationIcon.HairSalon
|
||||
"laundry" -> R.string.poi_category_laundry to LocationIcon.Laundromat
|
||||
|
||||
else -> R.string.poi_category_shopping to LocationIcon.Shopping
|
||||
}
|
||||
}
|
||||
|
||||
"amenity" -> values.firstNotNullOfOrNull {
|
||||
when (it) {
|
||||
"place_of_worship" -> matchAnyTag<Int, LocationIcon>("religion") {
|
||||
"christian" with (R.string.poi_category_church to LocationIcon.Church)
|
||||
"muslim" with (R.string.poi_category_mosque to LocationIcon.Mosque)
|
||||
"buddhist" with (R.string.poi_category_buddhist_temple to LocationIcon.BuddhistTemple)
|
||||
"hindu" with (R.string.poi_category_hindu_temple to LocationIcon.HinduTemple)
|
||||
"jewish" with (R.string.poi_category_synagogue to LocationIcon.Synagogue)
|
||||
} ?: (R.string.poi_category_place_of_worship to LocationIcon.PlaceOfWorship)
|
||||
|
||||
"fast_food" -> R.string.poi_category_fast_food to LocationIcon.FastFood
|
||||
"cafe" -> R.string.poi_category_cafe to LocationIcon.Cafe
|
||||
"ice_cream" -> R.string.poi_category_ice_cream to LocationIcon.IceCream
|
||||
"bar" -> R.string.poi_category_bar to LocationIcon.Bar
|
||||
"pub" -> R.string.poi_category_pub to LocationIcon.Pub
|
||||
"restaurant" -> matchAnyTag<Int, LocationIcon>("cuisine") {
|
||||
"pizza" with (R.string.poi_category_pizza_restaurant to LocationIcon.Pizza)
|
||||
"burger" with (R.string.poi_category_burger_restaurant to LocationIcon.Burger)
|
||||
"chinese" with (R.string.poi_category_chinese_restaurant to LocationIcon.Ramen)
|
||||
"ramen" with (R.string.poi_category_ramen_restaurant to LocationIcon.Ramen)
|
||||
"japanese" with (R.string.poi_category_japanese_restaurant to LocationIcon.JapaneseCuisine)
|
||||
"kebab" with (R.string.poi_category_kebab_restaurant to LocationIcon.Kebab)
|
||||
"asian" with (R.string.poi_category_asian_restaurant to LocationIcon.AsianCuisine)
|
||||
"soup" with (R.string.poi_category_soup_restaurant to LocationIcon.Soup)
|
||||
"coffee_shop" with (R.string.poi_category_cafe to LocationIcon.Cafe)
|
||||
"brunch" with (R.string.poi_category_brunch_restaurant to LocationIcon.Brunch)
|
||||
} ?: (R.string.poi_category_restaurant to LocationIcon.Restaurant)
|
||||
|
||||
"fuel" -> R.string.poi_category_fuel to LocationIcon.GasStation
|
||||
"car_rental" -> R.string.poi_category_car_rental to LocationIcon.CarRental
|
||||
"car_sharing" -> R.string.poi_category_car_sharing to LocationIcon.CarRental
|
||||
"car_wash" -> R.string.poi_category_car_wash to LocationIcon.CarWash
|
||||
"charging_station" -> R.string.poi_category_charging_station to LocationIcon.ChargingStation
|
||||
"parking", "parking_space", "motorcycle_parking" -> R.string.poi_category_parking to LocationIcon.Parking
|
||||
"motorcycle_rental" -> R.string.poi_category_motorcycle_rental to LocationIcon.Motorcycle
|
||||
|
||||
"theatre" -> R.string.poi_category_theater to LocationIcon.Theater
|
||||
"cinema" -> R.string.poi_category_cinema to LocationIcon.MovieTheater
|
||||
"nightclub" -> R.string.poi_category_nightclub to LocationIcon.NightClub
|
||||
"concert_hall" -> R.string.poi_category_concert_hall to LocationIcon.ConcertHall
|
||||
"casino" -> R.string.poi_category_casino to LocationIcon.Casino
|
||||
|
||||
"pharmacy" -> R.string.poi_category_pharmacy to LocationIcon.Pharmacy
|
||||
"bank" -> R.string.poi_category_bank to LocationIcon.Bank
|
||||
"atm" -> R.string.poi_category_atm to LocationIcon.Atm
|
||||
"doctors" -> R.string.poi_category_doctors to LocationIcon.Physician
|
||||
"dentist" -> R.string.poi_category_dentist to LocationIcon.Dentist
|
||||
"hospital" -> R.string.poi_category_hospital to LocationIcon.Hospital
|
||||
"clinic" -> R.string.poi_category_clinic to LocationIcon.Clinic
|
||||
|
||||
"police" -> R.string.poi_category_police to LocationIcon.Police
|
||||
"fire_station" -> R.string.poi_category_fire_station to LocationIcon.FireDepartment
|
||||
"courthouse" -> R.string.poi_category_courthouse to LocationIcon.Courthouse
|
||||
"post_office" -> R.string.poi_category_post_office to LocationIcon.PostOffice
|
||||
"library" -> R.string.poi_category_library to LocationIcon.Library
|
||||
"school" -> R.string.poi_category_school to LocationIcon.School
|
||||
"university" -> R.string.poi_category_university to LocationIcon.University
|
||||
"toilets" -> R.string.poi_category_toilets to LocationIcon.PublicBathroom
|
||||
"townhall" -> R.string.poi_category_townhall to LocationIcon.GovernmentBuilding
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
"tourism" -> values.firstNotNullOfOrNull {
|
||||
when (it) {
|
||||
"gallery" -> R.string.poi_category_gallery to LocationIcon.ArtGallery
|
||||
"museum" -> R.string.poi_category_museum to LocationIcon.Museum
|
||||
"theme_park" -> R.string.poi_category_amusement_park to LocationIcon.AmusementPark
|
||||
"hotel" -> R.string.poi_category_hotel to LocationIcon.Hotel
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
"leisure" -> values.firstNotNullOfOrNull {
|
||||
when (it) {
|
||||
"stadium" -> R.string.poi_category_stadium to LocationIcon.Stadium
|
||||
"fitness_centre" -> R.string.poi_category_fitness_center to LocationIcon.FitnessCenter
|
||||
"swimming_pool" -> R.string.poi_category_swimming to LocationIcon.Swimming
|
||||
"pitch", "sports_centre" -> matchAnyTag<Int, LocationIcon>("sport") {
|
||||
"soccer" with (R.string.poi_category_soccer to LocationIcon.Soccer)
|
||||
"tennis" with (R.string.poi_category_tennis to LocationIcon.Tennis)
|
||||
"basketball" with (R.string.poi_category_basketball to LocationIcon.Basketball)
|
||||
"gymnastics" with (R.string.poi_category_gymnastics to LocationIcon.Gymnastics)
|
||||
"martial_arts" with (R.string.poi_category_martial_arts to LocationIcon.MartialArts)
|
||||
"ice_hockey" with (R.string.poi_category_ice_hockey to LocationIcon.Hockey)
|
||||
"baseball" with (R.string.poi_category_baseball to LocationIcon.Baseball)
|
||||
"american_football" with (R.string.poi_category_american_football to LocationIcon.AmericanFootball)
|
||||
"handball" with (R.string.poi_category_handball to LocationIcon.Handball)
|
||||
"volleyball" with (R.string.poi_category_volleyball to LocationIcon.Volleyball)
|
||||
"skiing" with (R.string.poi_category_skiing to LocationIcon.Skiing)
|
||||
"cricket" with (R.string.poi_category_cricket to LocationIcon.Cricket)
|
||||
}
|
||||
|
||||
"golf_course" -> R.string.poi_category_golf to LocationIcon.Golf
|
||||
"park" -> R.string.poi_category_park to LocationIcon.Park
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
"historic" -> values.firstNotNullOfOrNull {
|
||||
when (it) {
|
||||
"monument" -> R.string.poi_category_monument to LocationIcon.Monument
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
"building" -> values.firstNotNullOfOrNull {
|
||||
when (it) {
|
||||
"government" -> R.string.poi_category_government_building to LocationIcon.GovernmentBuilding
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val (rid, icon) = category ?: return null to null
|
||||
return context.resources.getString(rid) to icon
|
||||
}
|
||||
|
||||
internal fun parseOpeningSchedule(
|
||||
openingHours: String?,
|
||||
localTime: LocalDateTime = LocalDateTime.now()
|
||||
): OpeningSchedule? {
|
||||
val parsed = openingHours?.toOpeningHoursOrNull(lenient = true) ?: return null
|
||||
|
||||
if (parsed.rules.singleOrNull()?.selector is TwentyFourSeven) {
|
||||
return OpeningSchedule.TwentyFourSeven
|
||||
}
|
||||
|
||||
val rangeRules = parsed.rules.mapNotNull { it.selector as? Range }
|
||||
|
||||
// Group rules by the weekdays they apply to. Rules can apply to multiple weekdays.
|
||||
val rulesMap = mutableMapOf<Weekday, MutableList<Range>>(
|
||||
Weekday.Monday to mutableListOf(),
|
||||
Weekday.Tuesday to mutableListOf(),
|
||||
Weekday.Wednesday to mutableListOf(),
|
||||
Weekday.Thursday to mutableListOf(),
|
||||
Weekday.Friday to mutableListOf(),
|
||||
Weekday.Saturday to mutableListOf(),
|
||||
Weekday.Sunday to mutableListOf()
|
||||
)
|
||||
|
||||
for (rule in rangeRules) {
|
||||
if (rule.weekdays != null) {
|
||||
for (selector in rule.weekdays!!) {
|
||||
when (selector) {
|
||||
is Weekday -> rulesMap[selector]!!.add(rule)
|
||||
is WeekdayRange -> {
|
||||
for (weekday in selector.start.ordinal..selector.end.ordinal) {
|
||||
rulesMap[Weekday.entries[weekday]]!!.add(rule)
|
||||
}
|
||||
}
|
||||
is SpecificWeekdays -> {
|
||||
rulesMap[selector.weekday]!!.add(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!rule.holidays.isNullOrEmpty()) {
|
||||
continue // skip PH and SH entries
|
||||
} else if (!rule.times.isNullOrEmpty() || !rule.months.isNullOrEmpty()) {
|
||||
rulesMap.forEach { _, it ->
|
||||
it.add(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out rules that are not valid for the current year, month, and week.
|
||||
val applicableRules = rulesMap.flatMap { (day, rules) ->
|
||||
rules.filterYears(localTime)
|
||||
.filterMonths(localTime)
|
||||
.filterNthDays(localTime)
|
||||
.map { it.copy(weekdays = listOf(day)) }
|
||||
}
|
||||
|
||||
val hours = mutableSetOf<OpeningHours>()
|
||||
|
||||
for (range in applicableRules) {
|
||||
|
||||
val localTimesWithDuration =
|
||||
range.times?.mapNotNull { it.toLocalTimeWithDuration() } ?: continue
|
||||
val daysOfWeek = range.weekdays
|
||||
.ifNullOrEmpty { Weekday.entries.toList() }
|
||||
.flatMap { it.toDaysOfWeek() }
|
||||
|
||||
hours += daysOfWeek.flatMap { dow ->
|
||||
localTimesWithDuration.map {
|
||||
val (start, dur) = it
|
||||
OpeningHours(dow, start, dur)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return OpeningSchedule.Hours(hours)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the rules that are valid for the given year.
|
||||
*/
|
||||
private fun List<Range>.filterYears(localTime: LocalDateTime): List<Range> {
|
||||
if (all { it.years.isNullOrEmpty() }) return this
|
||||
|
||||
val thisYear = filter {
|
||||
it.years?.any {
|
||||
when (it) {
|
||||
is Year -> it.year == localTime.year
|
||||
is StartingAtYear -> it.start <= localTime.year
|
||||
is YearRange -> localTime.year in it.start..it.end step (it.step ?: 1)
|
||||
}
|
||||
} == true
|
||||
}
|
||||
|
||||
if (!thisYear.isEmpty()) return thisYear
|
||||
|
||||
return filter { it.years.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the rules that are valid for the given month.
|
||||
*/
|
||||
private fun List<Range>.filterMonths(localTime: LocalDateTime): List<Range> {
|
||||
if (all { it.months.isNullOrEmpty() }) return this
|
||||
|
||||
val thisMonth = filter {
|
||||
it.months?.any {
|
||||
when (it) {
|
||||
is MonthRange -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal in it.start.ordinal..it.end.ordinal
|
||||
|
||||
is SingleMonth -> (it.year?.let { it == localTime.year } != false) && localTime.month.ordinal == it.month.ordinal
|
||||
|
||||
else -> false
|
||||
}
|
||||
} == true
|
||||
}
|
||||
|
||||
if (!thisMonth.isEmpty()) return thisMonth
|
||||
|
||||
return filter { it.months.isNullOrEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the rules that are valid for the given week.
|
||||
* (i.e. if the given week is the 2nd week of the month, it will return only the rules that are
|
||||
* valid for the 2nd week of the month)
|
||||
*/
|
||||
private fun List<Range>.filterNthDays(localTime: LocalDateTime): List<Range> {
|
||||
if (none { it.weekdays?.any { it is SpecificWeekdays } == true }) return this
|
||||
|
||||
val currentWeek = localTime.getNthWeekdaysOfCurrentWeek()
|
||||
|
||||
val specific = mapNotNull { range ->
|
||||
(range.weekdays?.singleOrNull() as? SpecificWeekdays)?.let {
|
||||
range to it
|
||||
}
|
||||
}
|
||||
|
||||
val rule = specific.firstOrNull { (_, specific) ->
|
||||
currentWeek.any { (dow, nthFwd, nthBwd) ->
|
||||
specific.weekday.ordinal == dow.ordinal && specific.nths.any {
|
||||
when (it) {
|
||||
is Nth -> it.nth == nthFwd
|
||||
is NthRange -> nthFwd in it.start..it.end
|
||||
is LastNth -> it.nth == nthBwd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rule != null) return listOf(rule.first)
|
||||
|
||||
return listOfNotNull(
|
||||
singleOrNull { it.weekdays?.singleOrNull() !is SpecificWeekdays }
|
||||
)
|
||||
}
|
||||
|
||||
private fun WeekdaysSelector.toDaysOfWeek(): List<DayOfWeek> {
|
||||
return when (this) {
|
||||
is Weekday -> listOf(
|
||||
when (this) {
|
||||
Weekday.Monday -> DayOfWeek.MONDAY
|
||||
Weekday.Tuesday -> DayOfWeek.TUESDAY
|
||||
Weekday.Wednesday -> DayOfWeek.WEDNESDAY
|
||||
Weekday.Thursday -> DayOfWeek.THURSDAY
|
||||
Weekday.Friday -> DayOfWeek.FRIDAY
|
||||
Weekday.Saturday -> DayOfWeek.SATURDAY
|
||||
Weekday.Sunday -> DayOfWeek.SUNDAY
|
||||
}
|
||||
)
|
||||
|
||||
is WeekdayRange -> (start to end).map { it.toDaysOfWeek().single().value }
|
||||
.into { start, end -> (start..end).map { DayOfWeek.of(it) } }
|
||||
|
||||
is SpecificWeekdays -> weekday.toDaysOfWeek()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun TimesSelector.toLocalTimeWithDuration(): Pair<LocalTime, Duration>? {
|
||||
if (this !is TimeSpan) return null
|
||||
|
||||
val start = start as? ClockTime ?: return null
|
||||
val end = end as? ExtendedClockTime ?: return null
|
||||
|
||||
return LocalTime.of(
|
||||
start.hour,
|
||||
start.minutes
|
||||
) to Duration.ofMinutes(
|
||||
(Math.floorMod(
|
||||
end.hour - start.hour,
|
||||
24
|
||||
) * 60 + end.minutes - start.minutes).toLong()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the ordinal position of each day of the current week (Monday to Sunday) within the month.
|
||||
*
|
||||
* @receiver LocalDateTime The current date and time.
|
||||
* @return A list of triples, where each triple contains:
|
||||
* - The `DayOfWeek` (e.g., Monday, Tuesday, etc.).
|
||||
* - The forward ordinal position of the day in the month (e.g., 1st Monday, 2nd Tuesday, etc.).
|
||||
* - The backward ordinal position of the day in the month (e.g., last Monday, 2nd last Tuesday, etc.).
|
||||
*
|
||||
* Example:
|
||||
* If today is the 15th of a month (Wednesday), the function will return:
|
||||
* - For Monday: `(DayOfWeek.MONDAY, 3, 2)` (3rd Monday of the month, 2nd last Monday of the month).
|
||||
* - For Wednesday: `(DayOfWeek.WEDNESDAY, 3, 2)` (3rd Wednesday of the month, 2nd last Wednesday of the month).
|
||||
*/
|
||||
private fun LocalDateTime.getNthWeekdaysOfCurrentWeek(): List<Triple<DayOfWeek, Int, Int>> {
|
||||
val monday = with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||
return (0 until 7).map { i ->
|
||||
val (nth, nthLast) = monday.plusDays(i.toLong()).let { weekday ->
|
||||
(
|
||||
ChronoUnit.WEEKS.between(
|
||||
with(TemporalAdjusters.firstInMonth(weekday.dayOfWeek)),
|
||||
weekday
|
||||
).toInt() + 1
|
||||
) to (
|
||||
ChronoUnit.WEEKS.between(
|
||||
weekday,
|
||||
with(TemporalAdjusters.lastInMonth(weekday.dayOfWeek))
|
||||
).toInt() + 1
|
||||
)
|
||||
}
|
||||
Triple(DayOfWeek.entries[i], nth, nthLast)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,253 @@
|
||||
package de.mm20.launcher2.locations.providers.googleapi
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.locations.providers.AndroidLocation
|
||||
import de.mm20.launcher2.locations.providers.LocationProvider
|
||||
import de.mm20.launcher2.openstreetmaps.R
|
||||
import de.mm20.launcher2.preferences.search.LocationSearchSettings
|
||||
import de.mm20.launcher2.search.Location
|
||||
import de.mm20.launcher2.search.ResultScore
|
||||
import de.mm20.launcher2.search.UpdateResult
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.net.UnknownHostException
|
||||
|
||||
private val Scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
private val HttpClient = OkHttpClient()
|
||||
|
||||
internal class OsmLocationProvider(
|
||||
private val context: Context,
|
||||
settings: LocationSearchSettings
|
||||
) : LocationProvider<Long> {
|
||||
|
||||
private val overpassApi = settings.overpassUrl.map {
|
||||
try {
|
||||
Retrofit.Builder()
|
||||
.client(HttpClient)
|
||||
.baseUrl(it?.takeIf { it.isNotBlank() }
|
||||
?: LocationSearchSettings.DefaultOverpassUrl)
|
||||
.addConverterFactory(OverpassQueryConverterFactory())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(OverpassApi::class.java)
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
}.stateIn(Scope, SharingStarted.Eagerly, null)
|
||||
|
||||
|
||||
suspend fun update(
|
||||
id: Long
|
||||
): UpdateResult<Location> = overpassApi.first()?.runCatching {
|
||||
this.search(
|
||||
OverpassIdQuery(
|
||||
id = id
|
||||
)
|
||||
).let {
|
||||
OsmLocation.fromOverpassResponse(it, context)
|
||||
}.first().apply {
|
||||
updatedSelf = { update(id) }
|
||||
}
|
||||
}?.fold(
|
||||
onSuccess = { UpdateResult.Success(it) },
|
||||
onFailure = {
|
||||
when (it) {
|
||||
is CancellationException, is UnknownHostException -> {
|
||||
// network
|
||||
UpdateResult.TemporarilyUnavailable(it)
|
||||
}
|
||||
|
||||
is HttpException -> when (it.code()) {
|
||||
in 400..499 -> UpdateResult.PermanentlyUnavailable(it)
|
||||
else -> UpdateResult.TemporarilyUnavailable(it)
|
||||
}
|
||||
|
||||
is NoSuchElementException -> {
|
||||
// empty response
|
||||
UpdateResult.PermanentlyUnavailable(it)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (it is Exception) {
|
||||
CrashReporter.logException(it)
|
||||
}
|
||||
UpdateResult.TemporarilyUnavailable(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
) ?: let {
|
||||
Log.e("OsmProvider", "overpassApi was not initialized")
|
||||
UpdateResult.TemporarilyUnavailable()
|
||||
}
|
||||
|
||||
override suspend fun search(
|
||||
query: String,
|
||||
userLocation: AndroidLocation,
|
||||
allowNetwork: Boolean,
|
||||
searchRadiusMeters: Int,
|
||||
hideUncategorized: Boolean,
|
||||
): List<Location> {
|
||||
if (!allowNetwork || query.length < 2) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
HttpClient.dispatcher.cancelAll()
|
||||
}
|
||||
|
||||
return overpassApi.first()?.runCatching {
|
||||
search(
|
||||
OverpassFuzzyRadiusQuery(
|
||||
query = query,
|
||||
tagGroups = delocalizeToQueryableTags(query),
|
||||
radius = searchRadiusMeters,
|
||||
latitude = userLocation.latitude,
|
||||
longitude = userLocation.longitude
|
||||
)
|
||||
)
|
||||
}?.onFailure {
|
||||
if (it !is HttpException && it !is CancellationException) {
|
||||
Log.e("OsmLocationProvider", "Failed to search for: $query", it)
|
||||
}
|
||||
}?.getOrNull()?.let {
|
||||
OsmLocation.fromOverpassResponse(it, context)
|
||||
}?.asSequence()?.filter {
|
||||
(!hideUncategorized || (it.category != null)) && it.distanceTo(userLocation) < searchRadiusMeters
|
||||
}?.groupBy {
|
||||
it.label.lowercase()
|
||||
}?.flatMap { (_, duplicates) ->
|
||||
// deduplicate results with same labels, if
|
||||
// - same category
|
||||
// - distance is less than 100m
|
||||
if (duplicates.size < 2) duplicates
|
||||
else {
|
||||
val luckyFirst = duplicates.first()
|
||||
duplicates
|
||||
.drop(1)
|
||||
.filter {
|
||||
it.category != luckyFirst.category ||
|
||||
it.distanceTo(luckyFirst) > 100.0
|
||||
} + luckyFirst
|
||||
}
|
||||
}?.sortedBy {
|
||||
it.distanceTo(userLocation)
|
||||
}?.take(9)?.toImmutableList() ?: emptyList()
|
||||
}
|
||||
|
||||
private val poiCategories = mapOf(
|
||||
R.string.poi_category_restaurant to "amenity=restaurant",
|
||||
R.string.poi_category_fast_food to "amenity=fast_food",
|
||||
R.string.poi_category_bar to "amenity=bar",
|
||||
R.string.poi_category_cafe to "amenity=cafe",
|
||||
R.string.poi_category_hotel to "tourism=hotel",
|
||||
R.string.poi_category_supermarket to "shop=supermarket",
|
||||
R.string.poi_category_school to "amenity=school",
|
||||
R.string.poi_category_parking to "amenity=parking",
|
||||
R.string.poi_category_fuel to "amenity=fuel",
|
||||
R.string.poi_category_toilets to "amenity=toilets",
|
||||
R.string.poi_category_pharmacy to "amenity=pharmacy",
|
||||
R.string.poi_category_hospital to "amenity=hospital",
|
||||
R.string.poi_category_post_office to "amenity=post_office",
|
||||
R.string.poi_category_pub to "amenity=pub",
|
||||
R.string.poi_category_doctors to "amenity=doctors",
|
||||
R.string.poi_category_police to "amenity=police",
|
||||
R.string.poi_category_dentist to "amenity=dentist",
|
||||
R.string.poi_category_library to "amenity=library",
|
||||
R.string.poi_category_ice_cream to "amenity=ice_cream",
|
||||
R.string.poi_category_theater to "amenity=theatre",
|
||||
R.string.poi_category_cinema to "amenity=cinema",
|
||||
R.string.poi_category_nightclub to "amenity=nightclub",
|
||||
R.string.poi_category_clinic to "amenity=clinic",
|
||||
R.string.poi_category_university to "amenity=university",
|
||||
R.string.poi_category_clothes to "shop=clothes",
|
||||
R.string.poi_category_convenience to "shop=convenience",
|
||||
R.string.poi_category_hairdresser to "shop=hairdresser",
|
||||
R.string.poi_category_books to "shop=books",
|
||||
R.string.poi_category_bakery to "shop=bakery",
|
||||
R.string.poi_category_car_rental to "amenity=car_rental",
|
||||
R.string.poi_category_car_sharing to "amenity=car_sharing",
|
||||
R.string.poi_category_mobile_phone to "shop=mobile_phone",
|
||||
R.string.poi_category_furniture to "shop=furniture",
|
||||
R.string.poi_category_alcohol to "shop=alcohol",
|
||||
R.string.poi_category_florist to "shop=florist",
|
||||
R.string.poi_category_mall to "shop=mall",
|
||||
R.string.poi_category_optician to "shop=optician",
|
||||
R.string.poi_category_jewelry to "shop=jewelry",
|
||||
R.string.poi_category_laundry to "amenity=laundry",
|
||||
R.string.poi_category_bank to "amenity=bank",
|
||||
R.string.poi_category_soccer to "leisure=pitch,sport=soccer",
|
||||
R.string.poi_category_basketball to "leisure=pitch,sport=basketball",
|
||||
R.string.poi_category_tennis to "leisure=pitch,sport=tennis",
|
||||
R.string.poi_category_atm to "amenity=atm",
|
||||
R.string.poi_category_kiosk to "shop=kiosk",
|
||||
R.string.poi_category_museum to "tourism=museum",
|
||||
R.string.poi_category_fitness_center to "leisure=fitness_centre",
|
||||
R.string.poi_category_church to "amenity=place_of_worship,religion=christian",
|
||||
R.string.poi_category_mosque to "amenity=place_of_worship,religion=muslim",
|
||||
R.string.poi_category_buddhist_temple to "amenity=place_of_worship,religion=buddhist",
|
||||
R.string.poi_category_hindu_temple to "amenity=place_of_worship,religion=hindu",
|
||||
R.string.poi_category_synagogue to "amenity=place_of_worship,religion=jewish",
|
||||
R.string.poi_category_pizza_restaurant to "amenity=restaurant,cuisine=pizza",
|
||||
R.string.poi_category_burger_restaurant to "amenity=restaurant,cuisine=burger",
|
||||
R.string.poi_category_place_of_worship to "amenity=place_of_worship",
|
||||
R.string.poi_category_chinese_restaurant to "amenity=restaurant,cuisine=chinese",
|
||||
R.string.poi_category_japanese_restaurant to "amenity=restaurant,cuisine=japanese",
|
||||
R.string.poi_category_kebab_restaurant to "amenity=restaurant,cuisine=kebab",
|
||||
R.string.poi_category_asian_restaurant to "amenity=restaurant,cuisine=asian",
|
||||
R.string.poi_category_ramen_restaurant to "amenity=restaurant,cuisine=ramen",
|
||||
R.string.poi_category_soup_restaurant to "amenity=restaurant,cuisine=soup",
|
||||
R.string.poi_category_brunch_restaurant to "amenity=restaurant,cuisine=brunch",
|
||||
R.string.poi_category_car_wash to "amenity=car_wash",
|
||||
R.string.poi_category_charging_station to "amenity=charging_station",
|
||||
R.string.poi_category_motorcycle_rental to "amenity=motorcycle_rental",
|
||||
R.string.poi_category_gallery to "tourism=gallery",
|
||||
R.string.poi_category_amusement_park to "tourism=theme_park",
|
||||
R.string.poi_category_concert_hall to "amenity=concert_hall",
|
||||
R.string.poi_category_stadium to "leisure=stadium",
|
||||
R.string.poi_category_casino to "amenity=casino",
|
||||
R.string.poi_category_discount_store to "shop=discount",
|
||||
R.string.poi_category_pet to "shop=pet",
|
||||
R.string.poi_category_shopping to "shop=mall",
|
||||
R.string.poi_category_swimming to "leisure=swimming_pool",
|
||||
R.string.poi_category_martial_arts to "leisure=sports_centre,sport=martial_arts",
|
||||
R.string.poi_category_golf to "leisure=golf_course",
|
||||
R.string.poi_category_gymnastics to "leisure=sports_hall,sport=gymnastics",
|
||||
R.string.poi_category_ice_hockey to "leisure=sports_centre,sport=ice_hockey",
|
||||
R.string.poi_category_baseball to "leisure=pitch,sport=baseball",
|
||||
R.string.poi_category_american_football to "leisure=pitch,sport=american_football",
|
||||
R.string.poi_category_handball to "leisure=pitch,sport=handball",
|
||||
R.string.poi_category_volleyball to "leisure=pitch,sport=volleyball",
|
||||
R.string.poi_category_skiing to "leisure=piste",
|
||||
R.string.poi_category_cricket to "leisure=pitch,sport=cricket",
|
||||
R.string.poi_category_park to "leisure=park",
|
||||
R.string.poi_category_monument to "historic=monument",
|
||||
R.string.poi_category_government_building to "building=government",
|
||||
R.string.poi_category_fire_station to "amenity=fire_station",
|
||||
R.string.poi_category_courthouse to "amenity=courthouse",
|
||||
R.string.poi_category_townhall to "amenity=townhall"
|
||||
).mapKeys { context.getString(it.key) }
|
||||
|
||||
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
||||
poiCategories.mapNotNull { (string, tags) ->
|
||||
val score = ResultScore(
|
||||
localizedQuery,
|
||||
primaryFields = listOf(string)
|
||||
)
|
||||
if (score.score > 0.8f) tags else null
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package de.mm20.launcher2.locations.providers.googleapi
|
||||
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
import java.lang.reflect.Type
|
||||
import kotlin.math.cos
|
||||
|
||||
/**
|
||||
* Overpass API query builder
|
||||
* Searches for nodes and ways that at least:
|
||||
* - match the query string in their name or brand tag
|
||||
* - match one of the given tag groups
|
||||
*/
|
||||
data class OverpassFuzzyRadiusQuery(
|
||||
/**
|
||||
* Free text query to search for.
|
||||
*/
|
||||
val query: String,
|
||||
/**
|
||||
* Tags groups to search for. Each item represents a group of tags, separated by commas.
|
||||
* The query matches if all tags in a group are present in the element.
|
||||
* For example:
|
||||
* ["amenity=restaurant,cuisine=italian", "amenity=cafe"]
|
||||
* This query will match elements that are either a restaurant with italian cuisine or a cafe.
|
||||
*/
|
||||
val tagGroups: List<String>,
|
||||
val radius: Int,
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
)
|
||||
|
||||
data class OverpassIdQuery(
|
||||
val id: Long,
|
||||
)
|
||||
|
||||
data class OverpassResponse(
|
||||
val elements: List<OverpassResponseElement>,
|
||||
)
|
||||
|
||||
data class OverpassResponseElementCenter(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
)
|
||||
|
||||
data class OverpassResponseElement(
|
||||
val type: String,
|
||||
val id: Long,
|
||||
val lat: Double?,
|
||||
val lon: Double?,
|
||||
val center: OverpassResponseElementCenter?,
|
||||
val tags: Map<String, String>?,
|
||||
)
|
||||
|
||||
interface OverpassApi {
|
||||
@POST("api/interpreter")
|
||||
suspend fun search(@Body data: OverpassFuzzyRadiusQuery): OverpassResponse
|
||||
|
||||
@POST("api/interpreter")
|
||||
suspend fun search(@Body data: OverpassIdQuery): OverpassResponse
|
||||
}
|
||||
|
||||
class OverpassFuzzyRadiusQueryConverter : Converter<OverpassFuzzyRadiusQuery, RequestBody> {
|
||||
override fun convert(value: OverpassFuzzyRadiusQuery): RequestBody {
|
||||
val encodedQuery = value.query.split(' ')
|
||||
.joinToString(
|
||||
separator = ".*",
|
||||
prefix = "\"",
|
||||
postfix = "\""
|
||||
) { Regex.escapeReplacement(it) }
|
||||
|
||||
val overpassQlBuilder = StringBuilder()
|
||||
val latDegreeChange = value.radius * 0.00001 / 1.11
|
||||
val lonDegreeChange = latDegreeChange / cos(Math.toRadians(value.latitude))
|
||||
val boundingBox = arrayOf(
|
||||
value.latitude - latDegreeChange, value.longitude - lonDegreeChange,
|
||||
value.latitude + latDegreeChange, value.longitude + lonDegreeChange
|
||||
)
|
||||
overpassQlBuilder.append("[out:json][timeout:10][bbox:" + boundingBox.joinToString(",") + "];")
|
||||
|
||||
overpassQlBuilder.append("(")
|
||||
overpassQlBuilder.append("nw[name~$encodedQuery,i];")
|
||||
overpassQlBuilder.append("nw[brand~$encodedQuery,i];")
|
||||
for (tag in value.tagGroups) {
|
||||
val tags = tag.split(',')
|
||||
|
||||
if (tags.isEmpty()) continue
|
||||
|
||||
overpassQlBuilder.append("nw[${tags.joinToString("][")}];")
|
||||
}
|
||||
overpassQlBuilder.append(");")
|
||||
// center to add the center coordinate of a way to the result, if applicable
|
||||
overpassQlBuilder.append("out center;")
|
||||
|
||||
return overpassQlBuilder.toString().toRequestBody()
|
||||
}
|
||||
}
|
||||
|
||||
class OverpassIdQueryConverter : Converter<OverpassIdQuery, RequestBody> {
|
||||
override fun convert(value: OverpassIdQuery): RequestBody = """
|
||||
[out:json];
|
||||
nw(${value.id});
|
||||
out center;
|
||||
""".trimIndent().toRequestBody()
|
||||
}
|
||||
|
||||
class OverpassQueryConverterFactory : Converter.Factory() {
|
||||
override fun requestBodyConverter(
|
||||
type: Type,
|
||||
parameterAnnotations: Array<out Annotation>,
|
||||
methodAnnotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<*, RequestBody>? {
|
||||
if (type == OverpassFuzzyRadiusQuery::class.java)
|
||||
return OverpassFuzzyRadiusQueryConverter()
|
||||
|
||||
if (type == OverpassIdQuery::class.java)
|
||||
return OverpassIdQueryConverter()
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ internal class OsmLocationProvider(
|
||||
searchRadiusMeters: Int,
|
||||
hideUncategorized: Boolean,
|
||||
): List<Location> {
|
||||
if (!allowNetwork || query.length < 2) {
|
||||
if (!allowNetwork || query.length < 1) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
@ -243,11 +243,20 @@ internal class OsmLocationProvider(
|
||||
).mapKeys { context.getString(it.key) }
|
||||
|
||||
private fun delocalizeToQueryableTags(localizedQuery: String): List<String> =
|
||||
|
||||
poiCategories.mapNotNull { (string, tags) ->
|
||||
val score = ResultScore(
|
||||
localizedQuery,
|
||||
primaryFields = listOf(string)
|
||||
)
|
||||
val score = if (string.contains("|")) {
|
||||
ResultScore(
|
||||
localizedQuery,
|
||||
primaryFields = string.split("|")
|
||||
)
|
||||
} else {
|
||||
ResultScore(
|
||||
localizedQuery,
|
||||
primaryFields = listOf(string)
|
||||
)
|
||||
}
|
||||
|
||||
if (score.score > 0.8f) tags else null
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,8 +231,12 @@ class NextcloudApiHelper(val context: Context) {
|
||||
.url("$server/ocs/v2.php/core/apppassword")
|
||||
.build()
|
||||
withContext(Dispatchers.IO) {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
response
|
||||
try {
|
||||
val response = httpClient.newCall(request).execute()
|
||||
response
|
||||
} catch (e: IOException) {
|
||||
Log.e("NextcloudApiHelper", "Error during Nextcloud logout", e)
|
||||
}
|
||||
}
|
||||
preferences.edit {
|
||||
putString("server", null)
|
||||
|
||||
@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProviderInfo
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherApps
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.widgets.CalendarWidget
|
||||
import de.mm20.launcher2.widgets.FavoritesWidget
|
||||
@ -32,6 +33,14 @@ class WidgetsService(
|
||||
for (profile in profiles) {
|
||||
widgets.addAll(appWidgetManager.getInstalledProvidersForProfile(profile))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// Ignore widgets that the launcher is not supposed to access
|
||||
widgets.filter {
|
||||
it.widgetFeatures and AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER == 0
|
||||
}
|
||||
} else {
|
||||
widgets
|
||||
}
|
||||
widgets
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user