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

This commit is contained in:
MM20 2025-07-04 11:56:05 +02:00
commit 26412bc721
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 119 additions and 54 deletions

View File

@ -288,7 +288,7 @@ class SearchVM : ViewModel(), KoinComponent {
locationResults.mergeWith( locationResults.mergeWith(
results.locations?.filterNot { hiddenKeys.contains(it.key) } results.locations?.filterNot { hiddenKeys.contains(it.key) }
?.let { locations -> ?.let { locations ->
devicePoseProvider.lastLocation?.let { devicePoseProvider.lastCachedLocation?.let {
locations.asSequence() locations.asSequence()
.sortedWith { a, b -> .sortedWith { a, b ->
a.distanceTo(it).compareTo(b.distanceTo(it)) a.distanceTo(it).compareTo(b.distanceTo(it))

View File

@ -167,7 +167,7 @@ fun LocationItem(
val userLocation by remember { val userLocation by remember {
viewModel.devicePoseProvider.getLocation() viewModel.devicePoseProvider.getLocation()
}.collectAsStateWithLifecycle(viewModel.devicePoseProvider.lastLocation) }.collectAsStateWithLifecycle(null)
val targetHeading by remember(userLocation, location) { val targetHeading by remember(userLocation, location) {
if (userLocation != null) { if (userLocation != null) {
@ -180,9 +180,7 @@ fun LocationItem(
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val userHeading by remember { val userHeading by remember {
if (userLocation != null) { viewModel.devicePoseProvider.getAzimuthDegrees()
viewModel.devicePoseProvider.getAzimuthDegrees()
} else emptyFlow()
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val icon by viewModel.icon.collectAsStateWithLifecycle() val icon by viewModel.icon.collectAsStateWithLifecycle()

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.devicepose
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.hardware.GeomagneticField
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
@ -14,33 +13,53 @@ import androidx.core.content.getSystemService
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
import de.mm20.launcher2.ktx.PI import de.mm20.launcher2.ktx.PI
import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.declination
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import de.mm20.launcher2.ktx.foldOrNull
import de.mm20.launcher2.ktx.isBetterThan
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
class DevicePoseProvider internal constructor( class DevicePoseProvider internal constructor(
private val context: Context private val context: Context
) { ) {
var lastLocation: Location? = null private val lastLocationLock = ReentrantReadWriteLock()
private set 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) { * @param skipCache: when using `getLocation().firstOrNull()`, prefer `skipCache = false`,
declination = GeomagneticField( * since otherwise, you may only receive an out of date location
location.latitude.toFloat(), */
location.longitude.toFloat(), fun getLocation(minTimeMs: Long = 1000, minDistanceM: Float = 1f, skipCache: Boolean = false) = channelFlow {
location.altitude.toFloat(), // have a local copy to work with
location.time var localLastLocation = lastCachedLocation
).declination
} 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 { val locationCallback = LocationListenerCompat {
lastLocation = it updateLocation(it)
updateDeclination(it) }
trySend(it)
if (!skipCache && localLastLocation != null) {
trySend(localLastLocation)
} }
context.getSystemService<LocationManager>() context.getSystemService<LocationManager>()
@ -50,20 +69,16 @@ class DevicePoseProvider internal constructor(
val hasCoarseAccess = val hasCoarseAccess =
context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION) context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
val location = val previousLocation =
(if (hasFineAccess) this@runCatching.getLastKnownLocation(LocationManager.GPS_PROVIDER) else null) hasFineAccess.foldOrNull { getLastKnownLocation(LocationManager.GPS_PROVIDER) } ?:
?: if (hasCoarseAccess) this@runCatching.getLastKnownLocation( hasCoarseAccess.foldOrNull { getLastKnownLocation(LocationManager.NETWORK_PROVIDER) }
LocationManager.NETWORK_PROVIDER
) else null
if (location != null) { if (previousLocation != null) {
lastLocation = location updateLocation(previousLocation)
updateDeclination(location)
trySend(location)
} }
if (hasFineAccess) { if (hasFineAccess) {
this@runCatching.requestLocationUpdates( requestLocationUpdates(
LocationManager.GPS_PROVIDER, LocationManager.GPS_PROVIDER,
minTimeMs, minTimeMs,
minDistanceM, minDistanceM,
@ -71,7 +86,7 @@ class DevicePoseProvider internal constructor(
) )
} }
if (hasCoarseAccess) { if (hasCoarseAccess) {
this@runCatching.requestLocationUpdates( requestLocationUpdates(
LocationManager.NETWORK_PROVIDER, LocationManager.NETWORK_PROVIDER,
minTimeMs, minTimeMs,
minDistanceM, minDistanceM,
@ -79,11 +94,12 @@ class DevicePoseProvider internal constructor(
) )
} }
}?.onFailure { }?.onFailure {
Log.e("SearchableItemVM", "Failed to register location listener", it) Log.e("DevicePoseProvider", "Failed to register location listener", it)
} }
awaitClose { awaitClose {
context.getSystemService<LocationManager>()?.removeUpdates(locationCallback) context.getSystemService<LocationManager>()?.removeUpdates(locationCallback)
lastCachedLocation = localLastLocation
} }
} }
@ -101,7 +117,7 @@ class DevicePoseProvider internal constructor(
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
SensorManager.getOrientation(rotationMatrix, orientationAngles) SensorManager.getOrientation(rotationMatrix, orientationAngles)
trySend(orientationAngles[0] * 180f / Float.PI + (declination ?: 0f)) trySend(orientationAngles[0] * 180f / Float.PI + (lastCachedLocation?.declination ?: 0f))
} }
} }

View File

@ -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

View File

@ -1,5 +1,14 @@
package de.mm20.launcher2.ktx package de.mm20.launcher2.ktx
inline fun Boolean.toInt(): Int { fun Boolean.toInt(): Int {
return if (this) 1 else 0 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 }

View File

@ -11,21 +11,15 @@ import de.mm20.launcher2.search.Location
import de.mm20.launcher2.search.SearchableRepository import de.mm20.launcher2.search.SearchableRepository
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.coroutineContext
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.newCoroutineContext
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
internal class LocationsRepository( internal class LocationsRepository(
@ -35,6 +29,7 @@ internal class LocationsRepository(
private val permissionsManager: PermissionsManager, private val permissionsManager: PermissionsManager,
) : SearchableRepository<Location> { ) : SearchableRepository<Location> {
@OptIn(FlowPreview::class)
override fun search( override fun search(
query: String, query: String,
allowNetwork: Boolean allowNetwork: Boolean
@ -45,16 +40,16 @@ internal class LocationsRepository(
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Location) val hasPermission = permissionsManager.hasPermission(PermissionGroup.Location)
return combineTransform(settings.data, hasPermission) { settingsData, permission -> return combineTransform(
poseProvider.getLocation(minDistanceM = 50.0f),
settings.data,
hasPermission
) { userLocation, settingsData, permission ->
emit(persistentListOf()) emit(persistentListOf())
if (!permission || settingsData.providers.isEmpty()) { if (!permission || settingsData.providers.isEmpty()) {
return@combineTransform return@combineTransform
} }
val userLocation = poseProvider.getLocation().firstOrNull()
?: poseProvider.lastLocation
?: return@combineTransform
val providers = settingsData.providers.map { val providers = settingsData.providers.map {
when (it) { when (it) {
"openstreetmaps" -> OsmLocationProvider(context, settings) "openstreetmaps" -> OsmLocationProvider(context, settings)

View File

@ -25,9 +25,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.toJavaDuration
interface WeatherRepository { interface WeatherRepository {
fun getActiveProvider(): Flow<WeatherProviderInfo?> fun getActiveProvider(): Flow<WeatherProviderInfo?>
@ -263,8 +261,9 @@ class WeatherUpdateWorker(
} }
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private suspend fun getLastKnownLocation(): LatLon? = private suspend fun getLastKnownLocation(): LatLon? = locationProvider.getLocation(skipCache = true)
locationProvider.getLocation().timeout(10.minutes).firstOrNull().or { .timeout(10.minutes)
locationProvider.lastLocation .firstOrNull()
}?.let { LatLon(it.latitude, it.longitude) } .or { locationProvider.lastCachedLocation }
?.let { LatLon(it.latitude, it.longitude) }
} }