Add OSM search provider (#611)
* add openstreetmaps module in data * fix injection * retrofit2 implementation * tokenization * partial rewrite of OpeningTime.fromOverpassElement * finish rewrite of OpeningTime.fromOverpassElement * fix merge x) * configurable search radius * add settings section and disable setting explicit timeout values for http-client to alleviate issues during debugging * settings screen localization * enable radius slider only when locations are enabled * fix dayRange parsing, add barebones UI * add files to git * add location listener in SearchableItemVM that gets activated by LocationItem * add heading listener * Calculations, UI additions * rename settings to LocationsSettings * use android location library for bearing calculations * location fix * rotation fix, demo UI * working buttons for launching map and website (if available) * finish botched UI * improve overpass query by utilizing regex for fuzzy search results * add link to documentation for further reference * localization comments * remove wikipedia minification setting * schema version, default radius 1.5km * move osm-specific opening-time parsing to OsmLocation * refactor with callbackFlow * remember flow and set minimum distance update to 1m * refactor for replacementIcon, add imperial unit option * 'open until' UI fix * implement serializers * catch errors in deserializer * hacky live sorting by distance * give max priority to bestmatch determined by SearchVM * add yards as additional step to metersToLocalizzedString * move http-client from serializers of osmlocation to companion object for cache updating * add setting for custom URL * round yards to int * add botched map preview * unbotch map tiles, draw user location in map (proof of concept) * - create MapTiles Composable - add border around map - add indicators for location and userlocation, when on map * fix default imperial units setting * fix tint color * add OSM attribution string * display loading animation when tiles can't be shown yet * create compose preview of maptiles * UI work * being glad that API's just return null instead of throwing information * tryStartActivity * aniimate card row placement * Text alignment, padding * Rotation -PI/PI wrap fix * fix direction arrow rotation when screen is upside down * more icons * icons, settings, localization - consider other tags than "amenity" when determining location category - add many more location categories with corresponding icons - add settings to disable map theming and hide search results with LocationCategory.OTHER - add default localizations for settings * catch errors when deserializing location category * move location and heading functions to Context.kt in extensions * fix hideUncategorized criterion * add pre-sorting by distance for location results in SearchVM by injecting Context into search() * specify receiver parameters in ktx.Context lambdas * move pose logic and context dependency to new module devicepose with DevicePoseProvider * git, add the frickin' module * search overpass for nodes and ways include category for parcel_locker already start searching for queries extending length 2 * make openingTimes immutable * OsmRepository changes - include telephone number - don't try to repeatedly update cache if there is no value to be updated to - deduplicate results with same label by category and distance (100m) - include fixmeurl to point to openstreetmaps.org/fixthemap * ask for center in overpass API to compute center coordinates of ways * search for brand * add chemist location category * restaurant / fastfood icon shenanigans * actually add the icons :| * add leisure tag for leisure:fitness_centre * return to 'open until'/'opens in'/'open next' * adding missing UI features - bug report dialog - call button if phone number exists - grid item popup * refactor to handle 24/7 locations more comfortably * hide hours in 'opens_in' when they are zero * show maptiles such that user is always in view * drawing adjustments * cache previous zoom level to speed up tile coordinate calculations * using remember * using MutableIntState * fix logic that determines whether tiles are loading * fix for numTiles == 9 * one plus one is two plus one makes three quick maths * animate user location indicator, remember calculations * fix off by one when determining next opening hour * second attempt to fix upside down arrow rotation (probably fine now) * logging * reconsider declination, inject samplingPeriod * undoing the merge undo * move localization string to i18n * revert reordering by distance * refactor .distanceTo * make Location abstract class to override compareTo with cached distance to correct sort order in search results * when it is if when you could use when * replace Pair with dataclass * condition check order * not creating objects with undefined locations, removing suspend from getCategory() * inject permissionsmanager as constructor parameter * Store OSM settings in decentralized datastore * Update searchable content in database on launch * Refactor, add mechanism to load updated searchable data lazily * Cache all OSM data in launcher database * Add pin to favorites button to location results * Add sealed class UpdateResult that is returned by awaiting updatedSelf of DeferredSearchable - update on success - set flag on temporarily unavailable (TODO add some UI indication) - delete and invalidate VM on permanently unavailable (Display some message window to user?) * Move sorting of Locations from OsmRepository to SearchVM using cached location in DevicePoseProvider, if available * make use of cached location in code * make use of DevicePoseProvider in WeatherRepository * inject via koin * increase getLocation().timeout() to 10 minutes since we are asking for locations only every hour, so 10 minutes seem reasonable (?) * poll new location every time * add icon for cached results where results are temporarily unavailable * Refactor DeferredSearchable to UpdatableSearchable that receives a closure to retrieve an updated self. - moved timestamp (formerly `updatedAt`) to UpdatableSearchable - moved logic whether to update searchable to `requestUpdatedSearchable` in SearchableItemVM, which gets triggered every time the details of the item are shown - keep track in SearchableVM whether we should retry updating, possibly bypassing a timestamp value that is not old enough - show toast upon permanently unavailable - animate "cached_searchable" icon - make "cached_searchable" icon clickable to show toast explaining the situation * logging on PermanentlyUnavailable * refactor OsmRepository.update() * MapTheming adjustments. There is now darkmode, hooray! * remove outdated comment * code tidying * remove unnecessary LaunchedEffect * make outdated badge only clickable when actually outdated * deserialize opening schedule * set deserialized props to null if strings are blank * also consider contact:* tagging scheme for website & phone * tweaks * git add Result.kt * don't search for locations if network is not allowed * merge fixes * Move location search settings to preferences module * Change wording and order of location search preferences * Limit location search results * Order location search results by distance * Use a sequence * Android Studio's suggestion wasn't as fleshed out as one would hope * Add proguard rules * Rename TileMapRepository to MapTileLoader --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,48 @@
|
||||
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "de.mm20.launcher2.location"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.materialcomponents.core)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
|
||||
implementation(project(":core:ktx"))
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,160 @@
|
||||
package de.mm20.launcher2.devicepose
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.hardware.GeomagneticField
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import de.mm20.launcher2.ktx.PI
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class DevicePoseProvider internal constructor(
|
||||
private val context: Context
|
||||
) {
|
||||
var lastLocation: Location? = null
|
||||
private set
|
||||
|
||||
private var declination: Float? = null
|
||||
private fun updateDeclination(location: Location) {
|
||||
declination = GeomagneticField(
|
||||
location.latitude.toFloat(),
|
||||
location.longitude.toFloat(),
|
||||
location.altitude.toFloat(),
|
||||
location.time
|
||||
).declination
|
||||
}
|
||||
|
||||
fun getLocation(minTimeMs: Long = 1000, minDistanceM: Float = 1f) = channelFlow {
|
||||
val locationCallback = LocationListener {
|
||||
lastLocation = it
|
||||
updateDeclination(it)
|
||||
trySend(it)
|
||||
}
|
||||
|
||||
context.getSystemService<LocationManager>()
|
||||
?.runCatching {
|
||||
val hasFineAccess =
|
||||
context.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
val hasCoarseAccess =
|
||||
context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
|
||||
val location =
|
||||
(if (hasFineAccess) this@runCatching.getLastKnownLocation(LocationManager.GPS_PROVIDER) else null)
|
||||
?: if (hasCoarseAccess) this@runCatching.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) else null
|
||||
|
||||
if (location != null) {
|
||||
lastLocation = location
|
||||
updateDeclination(location)
|
||||
trySend(location)
|
||||
}
|
||||
|
||||
if (hasFineAccess) {
|
||||
this@runCatching.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
minTimeMs,
|
||||
minDistanceM,
|
||||
locationCallback
|
||||
)
|
||||
}
|
||||
if (hasCoarseAccess) {
|
||||
this@runCatching.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
minTimeMs,
|
||||
minDistanceM,
|
||||
locationCallback
|
||||
)
|
||||
}
|
||||
}?.onFailure {
|
||||
Log.e("SearchableItemVM", "Failed to register location listener", it)
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
context.getSystemService<LocationManager>()?.removeUpdates(locationCallback)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAzimuthDegrees(samplingPeriodUs: Int = SensorManager.SENSOR_DELAY_UI): Flow<Float> = callbackFlow {
|
||||
val azimuthCallback = object : SensorEventListener {
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
if (event?.sensor?.type != Sensor.TYPE_ROTATION_VECTOR)
|
||||
return
|
||||
|
||||
val rotationMatrix = FloatArray(9)
|
||||
val orientationAngles = FloatArray(3)
|
||||
|
||||
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
|
||||
SensorManager.getOrientation(rotationMatrix, orientationAngles)
|
||||
|
||||
trySend(orientationAngles[0] * 180f / Float.PI + (declination ?: 0f))
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
.getSystemService<SensorManager>()
|
||||
?.runCatching {
|
||||
this@runCatching.registerListener(
|
||||
azimuthCallback,
|
||||
this@runCatching.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
|
||||
?: return@runCatching,
|
||||
samplingPeriodUs
|
||||
)
|
||||
}?.onFailure {
|
||||
Log.e("DevicePoseProvider", "Failed to register ROTATION_VECTOR listener", it)
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
context.getSystemService<SensorManager>()?.unregisterListener(azimuthCallback)
|
||||
}
|
||||
}
|
||||
|
||||
fun getHeadingToDegrees(headingEastwardDegrees: Float, samplingPeriodUs: Int = SensorManager.SENSOR_DELAY_UI): Flow<Float> = combine(
|
||||
getAzimuthDegrees(samplingPeriodUs),
|
||||
callbackFlow {
|
||||
val upsideDownCallback = object : SensorEventListener {
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
if (event?.sensor?.type != Sensor.TYPE_GRAVITY)
|
||||
return
|
||||
|
||||
val (_, _, z) = event.values
|
||||
trySend(z < 0f)
|
||||
}
|
||||
}
|
||||
context
|
||||
.getSystemService<SensorManager>()
|
||||
?.runCatching {
|
||||
this@runCatching.registerListener(
|
||||
upsideDownCallback,
|
||||
this@runCatching.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
?: return@runCatching,
|
||||
samplingPeriodUs
|
||||
)
|
||||
}?.onFailure {
|
||||
Log.e("SearchableItemVM", "Failed to register GRAVITY listener", it)
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
context.getSystemService<SensorManager>()?.unregisterListener(upsideDownCallback)
|
||||
}
|
||||
}) { azimuthDegrees, isUpsideDown ->
|
||||
|
||||
if (isUpsideDown) {
|
||||
azimuthDegrees - headingEastwardDegrees
|
||||
} else {
|
||||
headingEastwardDegrees - azimuthDegrees
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.mm20.launcher2.devicepose
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val devicePoseModule = module {
|
||||
single<DevicePoseProvider> { DevicePoseProvider(androidContext()) }
|
||||
}
|
||||
Reference in New Issue
Block a user