Merge branch 'main' of github.com:MM2-0/Kvaesitso

This commit is contained in:
MM20
2025-07-04 11:56:05 +02:00
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))
}
}
@@ -0,0 +1,48 @@
package de.mm20.launcher2.ktx
import android.hardware.GeomagneticField
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.nanoseconds
import android.location.Location as AndroidLocation
/* https://github.com/streetcomplete/StreetComplete/blob/master/app/src/main/java/de/westnordost/streetcomplete/util/location/LocationUtils.kt
* GPL-3.0-or-later
*/
fun AndroidLocation?.isBetterThan(previous: AndroidLocation?): Boolean {
if (this == null) return false
if (longitude.isNaN() || latitude.isNaN()) return false
if (previous == null) return true
val locationTimeDiff = elapsedRealtimeNanos.nanoseconds - previous.elapsedRealtimeNanos.nanoseconds
val isMuchNewer = locationTimeDiff > 2.minutes
val isMuchOlder = locationTimeDiff < (-2).minutes
val isNewer = locationTimeDiff.isPositive()
val accuracyDelta = accuracy - previous.accuracy
val isLessAccurate = accuracyDelta > 0f
val isMoreAccurate = accuracyDelta < 0f
val isMuchLessAccurate = accuracyDelta > 200f
val isFromSameProvider = provider == previous.provider
return when {
// the user has likely moved
isMuchNewer -> true
// If the new location is more than two minutes older, it must be worse
isMuchOlder -> false
isMoreAccurate -> true
isNewer && !isLessAccurate -> true
isNewer && !isMuchLessAccurate && isFromSameProvider -> true
else -> false
}
}
val AndroidLocation.declination: Float
get() = GeomagneticField(
latitude.toFloat(),
longitude.toFloat(),
altitude.toFloat(),
time
).declination
@@ -1,5 +1,14 @@
package de.mm20.launcher2.ktx
inline fun Boolean.toInt(): Int {
fun Boolean.toInt(): Int {
return if (this) 1 else 0
}
}
fun <T> Boolean.fold(
whenTrue: () -> T,
otherwise: () -> T
): T = if (this) whenTrue() else otherwise()
fun <T> Boolean.foldOrNull(
whenTrue: () -> T
): T? = fold(whenTrue) { null }