diff --git a/app/build.gradle b/app/build.gradle index 780738bb..72ca6da4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -205,6 +205,9 @@ dependencies { // implementation 'com.google.android.things:androidthings:1.0' implementation 'com.github.mik3y:usb-serial-for-android:3.4.6' + // location services + googleImplementation 'com.google.android.gms:play-services-location:19.0.1' + // For Firebase Crashlytics & Analytics googleImplementation 'com.google.firebase:firebase-crashlytics:18.2.6' googleImplementation 'com.google.firebase:firebase-analytics:20.1.0' diff --git a/app/src/main/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt b/app/src/fdroid/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt similarity index 100% rename from app/src/main/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt rename to app/src/fdroid/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt 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 new file mode 100644 index 00000000..cc29e64d --- /dev/null +++ b/app/src/google/java/com/geeksville/mesh/repository/location/SharedLocationManager.kt @@ -0,0 +1,87 @@ +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.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 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 = 1 * 60 * 1000L + + // Set up the Fused Location Provider and LocationRequest + private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context) + private val locationRequest = LocationRequest.create().apply { + interval = desiredInterval + fastestInterval = 30 * 1000L + maxWaitTime = 5 * 60 * 1000L + // smallestDisplacement = 30F // 30 meters + priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY + } + + @SuppressLint("MissingPermission") + private val _locationUpdates = callbackFlow { + val callback = object : LocationCallback() { + override fun onLocationResult(result: LocationResult) { + // info("New location: ${result.lastLocation}") + trySend(result.lastLocation) + } + } + if (!context.hasBackgroundPermission()) 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 + } +}