DevicePoseProvider: improve getLocation() (#1454)

* Only take updated location when it is of better quality

* Rename to AndroidLocation for clarification

* be pedantic about licenses

* fix race condition in DevicePoseProvider.kt

* add possibility to explicitly skip cache upon requesting location updates for WeatherRepository.kt

* LocationsRepository.kt: use getLocation() in combineTransform() to properly wait for & update on new locations

* LocationItem.kt: fix getAzimuthDegrees() usage for userHeading
This commit is contained in:
shtrophic
2025-07-04 11:55:20 +02:00
committed by GitHub
parent dd14c6cbaa
commit 31a1580db9
7 changed files with 119 additions and 54 deletions
@@ -2,7 +2,6 @@ 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
@@ -14,33 +13,53 @@ import androidx.core.content.getSystemService
import androidx.core.location.LocationListenerCompat
import de.mm20.launcher2.ktx.PI
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.declination
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.foldOrNull
import de.mm20.launcher2.ktx.isBetterThan
import kotlinx.coroutines.flow.combine
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
class DevicePoseProvider internal constructor(
private val context: Context
) {
var lastLocation: Location? = null
private set
private val lastLocationLock = ReentrantReadWriteLock()
var lastCachedLocation: Location? = null
get() { return lastLocationLock.read { field } }
private set(value) {
if (value == null) return
lastLocationLock.write {
if (value.isBetterThan(field)) {
field = value
}
}
}
private var declination: Float? = null
private fun updateDeclination(location: Location) {
declination = GeomagneticField(
location.latitude.toFloat(),
location.longitude.toFloat(),
location.altitude.toFloat(),
location.time
).declination
}
/**
* @param skipCache: when using `getLocation().firstOrNull()`, prefer `skipCache = false`,
* since otherwise, you may only receive an out of date location
*/
fun getLocation(minTimeMs: Long = 1000, minDistanceM: Float = 1f, skipCache: Boolean = false) = channelFlow {
// have a local copy to work with
var localLastLocation = lastCachedLocation
fun updateLocation(update: Location) {
if (!update.isBetterThan(localLastLocation)) return
localLastLocation = update
trySend(update)
}
fun getLocation(minTimeMs: Long = 1000, minDistanceM: Float = 1f) = channelFlow {
val locationCallback = LocationListenerCompat {
lastLocation = it
updateDeclination(it)
trySend(it)
updateLocation(it)
}
if (!skipCache && localLastLocation != null) {
trySend(localLastLocation)
}
context.getSystemService<LocationManager>()
@@ -50,20 +69,16 @@ class DevicePoseProvider internal constructor(
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
val previousLocation =
hasFineAccess.foldOrNull { getLastKnownLocation(LocationManager.GPS_PROVIDER) } ?:
hasCoarseAccess.foldOrNull { getLastKnownLocation(LocationManager.NETWORK_PROVIDER) }
if (location != null) {
lastLocation = location
updateDeclination(location)
trySend(location)
if (previousLocation != null) {
updateLocation(previousLocation)
}
if (hasFineAccess) {
this@runCatching.requestLocationUpdates(
requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMs,
minDistanceM,
@@ -71,7 +86,7 @@ class DevicePoseProvider internal constructor(
)
}
if (hasCoarseAccess) {
this@runCatching.requestLocationUpdates(
requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
minTimeMs,
minDistanceM,
@@ -79,11 +94,12 @@ class DevicePoseProvider internal constructor(
)
}
}?.onFailure {
Log.e("SearchableItemVM", "Failed to register location listener", it)
Log.e("DevicePoseProvider", "Failed to register location listener", it)
}
awaitClose {
context.getSystemService<LocationManager>()?.removeUpdates(locationCallback)
lastCachedLocation = localLastLocation
}
}
@@ -101,7 +117,7 @@ class DevicePoseProvider internal constructor(
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
trySend(orientationAngles[0] * 180f / Float.PI + (declination ?: 0f))
trySend(orientationAngles[0] * 180f / Float.PI + (lastCachedLocation?.declination ?: 0f))
}
}