From 8f5400c33b5c6f83e00327130c695271c2d94aa9 Mon Sep 17 00:00:00 2001 From: andrekir Date: Wed, 10 Jan 2024 06:25:54 -0300 Subject: [PATCH] refactor: consolidate location APIs into `LocationManagerCompat` --- app/build.gradle | 3 - .../location/SharedLocationManager.kt | 91 ------------------- .../location/SharedLocationManager.kt | 90 ------------------ .../repository/location/LocationRepository.kt | 81 ++++++++++++++++- .../location/LocationRepositoryModule.kt | 11 +-- 5 files changed, 81 insertions(+), 195 deletions(-) delete mode 100644 app/src/fdroid/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt delete mode 100644 app/src/google/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt diff --git a/app/build.gradle b/app/build.gradle index d1ec3a2a5..6f604884e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/fdroid/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt b/app/src/fdroid/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt deleted file mode 100644 index cfa34871d..000000000 --- a/app/src/fdroid/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt +++ /dev/null @@ -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 = MutableStateFlow(false) - val receivingLocationUpdates: StateFlow 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 { - return _locationUpdates - } -} diff --git a/app/src/google/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt b/app/src/google/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt deleted file mode 100644 index 4b525bd14..000000000 --- a/app/src/google/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt +++ /dev/null @@ -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 = MutableStateFlow(false) - val receivingLocationUpdates: StateFlow 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 { - return _locationUpdates - } -} diff --git a/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt index 4d20c7036..a0dda41d7 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepository.kt @@ -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, +) : Logging { + /** * Status of whether the app is actively subscribed to location changes. */ - val receivingLocationUpdates: StateFlow = sharedLocationManager.receivingLocationUpdates + private val _receivingLocationUpdates: MutableStateFlow = MutableStateFlow(false) + val receivingLocationUpdates: StateFlow 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() } diff --git a/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepositoryModule.kt index d49cf660e..c11ddfeb8 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/location/LocationRepositoryModule.kt @@ -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) -} \ No newline at end of file + fun provideLocationManager(@ApplicationContext context: Context): LocationManager = + context.applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager +}