mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: consolidate location APIs into LocationManagerCompat
This commit is contained in:
parent
0e3939f378
commit
8f5400c33b
5 changed files with 81 additions and 195 deletions
|
|
@ -223,9 +223,6 @@ dependencies {
|
|||
// For UART access
|
||||
implementation 'com.github.mik3y:usb-serial-for-android:3.7.0'
|
||||
|
||||
// location services
|
||||
googleImplementation 'com.google.android.gms:play-services-location:21.0.1'
|
||||
|
||||
// For Firebase Crashlytics & Analytics
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx'
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
package com.geeksville.mesh.repository.location
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.hasBackgroundPermission
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
||||
/**
|
||||
* Wraps LocationCallback() in callbackFlow
|
||||
*
|
||||
* Derived in part from https://github.com/android/location-samples/blob/main/LocationUpdatesBackgroundKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesbackgroundkotlin/data/MyLocationManager.kt
|
||||
* and https://github.com/googlecodelabs/kotlin-coroutines/blob/master/ktx-library-codelab/step-06/myktxlibrary/src/main/java/com/example/android/myktxlibrary/LocationUtils.kt
|
||||
*/
|
||||
class SharedLocationManager constructor(
|
||||
private val context: Context,
|
||||
externalScope: CoroutineScope
|
||||
) : Logging {
|
||||
|
||||
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
|
||||
|
||||
// Defaults from device positionBroadcastSmart
|
||||
private val timeTravelMinimum = 30 * 1000L // 30 seconds
|
||||
private val distanceTravelMinimum = 0f
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private val _locationUpdates = callbackFlow {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val callback = LocationListenerCompat { location ->
|
||||
// info("New location: $location")
|
||||
trySend(location)
|
||||
}
|
||||
|
||||
if (!context.hasBackgroundPermission()) close()
|
||||
|
||||
val providerList = buildList {
|
||||
val providers = locationManager.allProviders
|
||||
if (android.os.Build.VERSION.SDK_INT >= 31 && LocationManager.FUSED_PROVIDER in providers) {
|
||||
add(LocationManager.FUSED_PROVIDER)
|
||||
} else {
|
||||
if (LocationManager.NETWORK_PROVIDER in providers) add(LocationManager.NETWORK_PROVIDER)
|
||||
if (LocationManager.GPS_PROVIDER in providers) add(LocationManager.GPS_PROVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
info("Starting location updates with $providerList minTimeMs=${timeTravelMinimum}ms and minDistanceM=${distanceTravelMinimum}m")
|
||||
_receivingLocationUpdates.value = true
|
||||
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
|
||||
|
||||
try {
|
||||
providerList.forEach { provider ->
|
||||
locationManager.requestLocationUpdates(
|
||||
provider,
|
||||
timeTravelMinimum,
|
||||
distanceTravelMinimum,
|
||||
callback,
|
||||
context.mainLooper
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
close(e) // in case of exception, close the Flow
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
info("Stopping location requests")
|
||||
_receivingLocationUpdates.value = false
|
||||
GeeksvilleApplication.analytics.track("location_stop")
|
||||
locationManager.removeUpdates(callback) // clean up when Flow collection ends
|
||||
}
|
||||
}.shareIn(
|
||||
externalScope,
|
||||
replay = 0,
|
||||
started = SharingStarted.WhileSubscribed()
|
||||
)
|
||||
|
||||
fun locationFlow(): Flow<Location> {
|
||||
return _locationUpdates
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
package com.geeksville.mesh.repository.location
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.os.Looper
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.hasBackgroundPermission
|
||||
import com.geeksville.mesh.android.isGooglePlayAvailable
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.location.Priority
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
||||
/**
|
||||
* Wraps LocationCallback() in callbackFlow
|
||||
*
|
||||
* Derived in part from https://github.com/android/location-samples/blob/main/LocationUpdatesBackgroundKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesbackgroundkotlin/data/MyLocationManager.kt
|
||||
* and https://github.com/googlecodelabs/kotlin-coroutines/blob/master/ktx-library-codelab/step-06/myktxlibrary/src/main/java/com/example/android/myktxlibrary/LocationUtils.kt
|
||||
*/
|
||||
class SharedLocationManager constructor(
|
||||
private val context: Context,
|
||||
externalScope: CoroutineScope
|
||||
) : Logging {
|
||||
|
||||
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
|
||||
|
||||
private val desiredInterval = 30 * 1000L // 30 seconds
|
||||
|
||||
// Set up the Fused Location Provider and LocationRequest
|
||||
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
|
||||
private val locationRequest = LocationRequest.Builder(desiredInterval)
|
||||
.setMinUpdateIntervalMillis(desiredInterval / 2)
|
||||
.setMaxUpdateDelayMillis(desiredInterval)
|
||||
// .setMinUpdateDistanceMeters(30f) // 30 meters
|
||||
.setPriority(Priority.PRIORITY_HIGH_ACCURACY)
|
||||
.build()
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private val _locationUpdates = callbackFlow {
|
||||
val callback = object : LocationCallback() {
|
||||
override fun onLocationResult(result: LocationResult) {
|
||||
// info("New location: ${result.lastLocation}")
|
||||
result.lastLocation?.let { lastLocation ->
|
||||
trySend(lastLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!context.hasBackgroundPermission() || !isGooglePlayAvailable(context)) close()
|
||||
|
||||
info("Starting location requests with interval=${desiredInterval}ms")
|
||||
_receivingLocationUpdates.value = true
|
||||
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
|
||||
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
locationRequest,
|
||||
callback,
|
||||
Looper.getMainLooper()
|
||||
).addOnFailureListener { ex ->
|
||||
errormsg("Failed to listen to GPS error: ${ex.message}")
|
||||
close(ex) // in case of exception, close the Flow
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
info("Stopping location requests")
|
||||
_receivingLocationUpdates.value = false
|
||||
GeeksvilleApplication.analytics.track("location_stop")
|
||||
fusedLocationClient.removeLocationUpdates(callback) // clean up when Flow collection ends
|
||||
}
|
||||
}.shareIn(
|
||||
externalScope,
|
||||
replay = 0,
|
||||
started = SharingStarted.WhileSubscribed()
|
||||
)
|
||||
|
||||
fun locationFlow(): Flow<Location> {
|
||||
return _locationUpdates
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,91 @@
|
|||
package com.geeksville.mesh.repository.location
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.location.LocationManager
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.location.LocationRequestCompat
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.hasBackgroundPermission
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class LocationRepository @Inject constructor(
|
||||
private val sharedLocationManager: SharedLocationManager
|
||||
) {
|
||||
private val context: Application,
|
||||
private val locationManager: dagger.Lazy<LocationManager>,
|
||||
) : Logging {
|
||||
|
||||
/**
|
||||
* Status of whether the app is actively subscribed to location changes.
|
||||
*/
|
||||
val receivingLocationUpdates: StateFlow<Boolean> = sharedLocationManager.receivingLocationUpdates
|
||||
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun LocationManager.requestLocationUpdates() = callbackFlow {
|
||||
if (!context.hasBackgroundPermission()) close()
|
||||
|
||||
val intervalMs = 30 * 1000L // 30 seconds
|
||||
val minDistanceM = 0f
|
||||
|
||||
val locationRequest = LocationRequestCompat.Builder(intervalMs)
|
||||
.setMinUpdateDistanceMeters(minDistanceM)
|
||||
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
|
||||
.build()
|
||||
|
||||
val locationListener = LocationListenerCompat { location ->
|
||||
// info("New location: $location")
|
||||
trySend(location)
|
||||
}
|
||||
|
||||
val providerList = buildList {
|
||||
val providers = allProviders
|
||||
if (android.os.Build.VERSION.SDK_INT >= 31 && LocationManager.FUSED_PROVIDER in providers) {
|
||||
add(LocationManager.FUSED_PROVIDER)
|
||||
} else {
|
||||
if (LocationManager.GPS_PROVIDER in providers) add(LocationManager.GPS_PROVIDER)
|
||||
if (LocationManager.NETWORK_PROVIDER in providers) add(LocationManager.NETWORK_PROVIDER)
|
||||
}
|
||||
}
|
||||
|
||||
info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
|
||||
_receivingLocationUpdates.value = true
|
||||
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
|
||||
|
||||
try {
|
||||
providerList.forEach { provider ->
|
||||
LocationManagerCompat.requestLocationUpdates(
|
||||
this@requestLocationUpdates,
|
||||
provider,
|
||||
locationRequest,
|
||||
Dispatchers.IO.asExecutor(),
|
||||
locationListener,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
close(e) // in case of exception, close the Flow
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
info("Stopping location requests")
|
||||
_receivingLocationUpdates.value = false
|
||||
GeeksvilleApplication.analytics.track("location_stop")
|
||||
|
||||
LocationManagerCompat.removeUpdates(this@requestLocationUpdates, locationListener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable flow for location updates
|
||||
*/
|
||||
fun getLocations() = sharedLocationManager.locationFlow()
|
||||
fun getLocations() = locationManager.get().requestLocationUpdates()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
package com.geeksville.mesh.repository.location
|
||||
|
||||
import android.content.Context
|
||||
import android.location.LocationManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import javax.inject.Singleton
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object LocationRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedLocationManager(
|
||||
@ApplicationContext context: Context
|
||||
): SharedLocationManager = SharedLocationManager(context, GlobalScope)
|
||||
}
|
||||
fun provideLocationManager(@ApplicationContext context: Context): LocationManager =
|
||||
context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue