move location service to repository

pull/433/head
andrekir 2022-05-20 09:13:59 -03:00
rodzic d50e9e1644
commit 6bda993851
8 zmienionych plików z 160 dodań i 222 usunięć

Wyświetl plik

@ -120,7 +120,7 @@ interface IMeshService {
void setRegion(int regionCode);
/// Start providing location (from phone GPS) to mesh
void setupProvideLocation();
void startProvideLocation();
/// Stop providing location (from phone GPS) to mesh
void stopProvideLocation();

Wyświetl plik

@ -683,7 +683,7 @@ class MainActivity : BaseActivity(), Logging,
}
// if provideLocation enabled: Start providing location (from phone GPS) to mesh
if (model.provideLocation.value == true)
service.setupProvideLocation()
service.startProvideLocation()
}
} else {
// For other connection states, just slam them in

Wyświetl plik

@ -0,0 +1,18 @@
package com.geeksville.mesh.repository.location
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
class LocationRepository @Inject constructor(
private val sharedLocationManager: SharedLocationManager
) {
/**
* Status of whether the app is actively subscribed to location changes.
*/
val receivingLocationUpdates: StateFlow<Boolean> = sharedLocationManager.receivingLocationUpdates
/**
* Observable flow for location updates
*/
fun getLocations() = sharedLocationManager.locationFlow()
}

Wyświetl plik

@ -0,0 +1,22 @@
package com.geeksville.mesh.repository.location
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.GlobalScope
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object LocationRepositoryModule {
@Provides
@Singleton
fun provideSharedLocationManager(
@ApplicationContext context: Context
): SharedLocationManager =
SharedLocationManager(context, GlobalScope)
}

Wyświetl plik

@ -0,0 +1,89 @@
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.android.GeeksvilleApplication
import com.geeksville.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<Boolean> = MutableStateFlow(false)
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
// TODO use positionBroadcastSecs / test locationRequest settings
private val desiredInterval = 1 * 60 * 1000L
// if unset, use positionBroadcastSecs default
// positionBroadcastSecs.takeIf { it != 0L }?.times(1000L) ?: (15 * 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
// smallestDisplacement = 50F // 50 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<Location> {
return _locationUpdates
}
}

Wyświetl plik

@ -1,14 +1,10 @@
package com.geeksville.mesh.service
import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.Looper
import android.os.RemoteException
import android.widget.Toast
import androidx.annotation.UiThread
import androidx.core.content.edit
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
@ -22,22 +18,19 @@ import com.geeksville.mesh.android.hasBackgroundPermission
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.repository.radio.BluetoothInterface
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.RadioServiceConnectionState
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.util.*
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.LocationSettingsRequest
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.json.Json
import java.util.*
import javax.inject.Inject
@ -65,6 +58,9 @@ class MeshService : Service(), Logging {
@Inject
lateinit var usbRepository: Lazy<UsbRepository>
@Inject
lateinit var locationRepository: LocationRepository
companion object : Logging {
/// Intents broadcast by MeshService
@ -133,17 +129,11 @@ class MeshService : Service(), Logging {
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private var connectionState = ConnectionState.DISCONNECTED
private var fusedLocationClient: FusedLocationProviderClient? = null
private var locationFlow: Job? = null
// If we've ever read a valid region code from our device it will be here
var curRegionValue = RadioConfigProtos.RegionCode.Unset_VALUE
private val locationCallback = MeshServiceLocationCallback(
::sendPositionScoped,
onSendPositionFailed = { onConnectionChanged(ConnectionState.DEVICE_SLEEP) },
getNodeNum = { myNodeNum }
)
private fun getSenderName(packet: DataPacket?): String {
val name = nodeDBbyID[packet?.from]?.user?.longName
return name ?: "Unknown username"
@ -159,109 +149,32 @@ class MeshService : Service(), Logging {
ConnectionState.DEVICE_SLEEP -> getString(R.string.device_sleeping)
}
private fun warnUserAboutLocation() {
Toast.makeText(
this,
getString(R.string.location_disabled),
Toast.LENGTH_LONG
).show()
}
private var locationIntervalMsec = 0L
/**
* a periodic callback that perhaps send our position to other nodes.
* We first check to see if our local device has already sent a position and if so, we punt until the next check.
* This allows us to only 'fill in' with GPS positions when the local device happens to have no good GPS sats.
*/
private fun sendPositionScoped(
lat: Double = 0.0,
lon: Double = 0.0,
alt: Int = 0,
destNum: Int = DataPacket.NODENUM_BROADCAST,
wantResponse: Boolean = false
) {
// This operation can take a while, so instead of staying in the callback (location services) context
// do most of the work in my service thread
serviceScope.handledLaunch {
// if android called us too soon, just ignore
sendPosition(lat, lon, alt, destNum, wantResponse)
}
}
/**
* start our location requests (if they weren't already running)
*
* per https://developer.android.com/training/location/change-location-settings
* & https://developer.android.com/training/location/request-updates
*/
@SuppressLint("MissingPermission")
@UiThread
private fun startLocationRequests(requestInterval: Long) {
// FIXME - currently we don't support location reading without google play
if (fusedLocationClient == null && hasBackgroundPermission() && isGooglePlayAvailable(this)) {
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
private fun startLocationRequests() {
// If we're already observing updates, don't register again
if (locationFlow?.isActive == true) return
locationIntervalMsec = requestInterval
val request = LocationRequest.create().apply {
interval = requestInterval
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(request)
val locationClient = LocationServices.getSettingsClient(this)
val locationSettingsResponse = locationClient.checkLocationSettings(builder.build())
locationSettingsResponse.addOnSuccessListener {
debug("We are now successfully listening to the GPS")
}
locationSettingsResponse.addOnFailureListener { exception ->
errormsg("Failed to listen to GPS")
when (exception) {
is ResolvableApiException ->
exceptionReporter {
// Location settings are not satisfied, but this can be fixed
// by showing the user a dialog.
// Show the dialog by calling startResolutionForResult(),
// and check the result in onActivityResult().
// exception.startResolutionForResult(this@MainActivity, REQUEST_CHECK_SETTINGS)
// For now just punt and show a dialog
warnUserAboutLocation()
}
is ApiException ->
when (exception.statusCode) {
17 ->
// error: cancelled by user
errormsg("User cancelled location access", exception)
8502 ->
// error: settings change unavailable
errormsg(
"Settings-change-unavailable, user disabled location access (globally?)",
exception
)
else ->
Exceptions.report(exception)
}
else ->
Exceptions.report(exception)
if (hasBackgroundPermission() && isGooglePlayAvailable(this)) {
locationFlow = locationRepository.getLocations()
.onEach { location ->
sendPosition(
location.latitude,
location.longitude,
location.altitude.toInt(),
myNodeNum, // we just send to the local node
false // and we never want ACKs
)
}
}
val client = LocationServices.getFusedLocationProviderClient(this)
client.requestLocationUpdates(request, locationCallback, Looper.getMainLooper())
fusedLocationClient = client
.launchIn(CoroutineScope(Dispatchers.Default))
}
}
private fun stopLocationRequests() {
if (fusedLocationClient != null) {
if (locationFlow?.isActive == true) {
debug("Stopping location requests")
GeeksvilleApplication.analytics.track("location_stop")
fusedLocationClient?.removeLocationUpdates(locationCallback)
fusedLocationClient = null
locationFlow?.cancel()
}
}
@ -1022,39 +935,6 @@ class MeshService : Service(), Logging {
maybeUpdateServiceStatusNotification()
}
private fun setupLocationRequests() {
stopLocationRequests()
val mi = myNodeInfo
val prefs = radioConfig?.preferences
if (mi != null && prefs != null) {
val broadcastSecs = prefs.positionBroadcastSecs
var desiredInterval = if (broadcastSecs == 0) // unset by device, use default
15 * 60 * 1000L
else
broadcastSecs * 1000L
if (prefs.locationShareDisabled) {
info("GPS location sharing is disabled")
desiredInterval = 0
}
// if (prefs.fixedPosition) {
// info("Node has fixed position, therefore not overriding position")
// desiredInterval = 0
// }
if (desiredInterval != 0L) {
info("desired GPS assistance interval $desiredInterval")
startLocationRequests(desiredInterval)
} else {
info("No GPS assistance desired, but sending UTC time to mesh")
warnUserAboutLocation()
sendPosition()
}
}
}
/**
* Send in analytics about mesh connection
*/
@ -1126,6 +1006,9 @@ class MeshService : Service(), Logging {
// Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy)
saveSettings()
// lost radio connection, therefore no need to keep listening to GPS
stopLocationRequests()
GeeksvilleApplication.analytics.track(
"mesh_disconnect",
DataPair("num_nodes", numNodes),
@ -1518,7 +1401,6 @@ class MeshService : Service(), Logging {
/**
* Send a position (typically from our built in GPS) into the mesh.
* Must be called from serviceScope. Use sendPositionScoped() for direct calls.
*/
private fun sendPosition(
lat: Double = 0.0,
@ -1843,14 +1725,13 @@ class MeshService : Service(), Logging {
r.toString()
}
override fun setupProvideLocation() = toRemoteExceptions {
setupLocationRequests()
override fun startProvideLocation() = toRemoteExceptions {
startLocationRequests()
}
override fun stopProvideLocation() = toRemoteExceptions {
stopLocationRequests()
}
}
}

Wyświetl plik

@ -1,72 +0,0 @@
package com.geeksville.mesh.service
import android.location.Location
import android.os.RemoteException
import com.geeksville.mesh.DataPacket
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationResult
val Location.isAccurateForMesh: Boolean get() = !this.hasAccuracy() || this.accuracy < 200
private fun List<Location>.filterAccurateForMesh() = filter { it.isAccurateForMesh }
private fun LocationResult.lastLocationOrBestEffort(): Location? {
return lastLocation ?: locations.filterAccurateForMesh().lastOrNull()
}
typealias SendPosition = (Double, Double, Int, Int, Boolean) -> Unit // Lat, Lon, alt, destNum, wantResponse
typealias OnSendFailure = () -> Unit
typealias GetNodeNum = () -> Int
class MeshServiceLocationCallback(
private val onSendPosition: SendPosition,
private val onSendPositionFailed: OnSendFailure,
private val getNodeNum: GetNodeNum
) : LocationCallback() {
companion object {
const val DEFAULT_SEND_RATE_LIMIT = 30
}
private var lastSendTimeMs: Long = 0L
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
locationResult.lastLocationOrBestEffort()?.let { location ->
MeshService.info("got phone location")
if (location.isAccurateForMesh) { // if within 200 meters, or accuracy is unknown
try {
// Do we want to broadcast this position globally, or are we just telling the local node what its current position is
val shouldBroadcast =
false // no need to rate limit, because we are just sending to the local node
val destinationNumber =
if (shouldBroadcast) DataPacket.NODENUM_BROADCAST else getNodeNum()
// Note: we never want this message sent as a reliable message, because it is low value and we'll be sending one again later anyways
sendPosition(location, destinationNumber, wantResponse = false)
} catch (ex: RemoteException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
MeshService.warn("Lost connection to radio, stopping location requests")
onSendPositionFailed()
} catch (ex: BLEException) { // Really a RadioNotConnected exception, but it has changed into this type via remoting
MeshService.warn("BLE exception, stopping location requests $ex")
onSendPositionFailed()
}
} else {
MeshService.warn("accuracy ${location.accuracy} is too poor to use")
}
}
}
private fun sendPosition(location: Location, destinationNumber: Int, wantResponse: Boolean) {
onSendPosition(
location.latitude,
location.longitude,
location.altitude.toInt(),
destinationNumber,
wantResponse // wantResponse?
)
}
}

Wyświetl plik

@ -826,7 +826,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
debug("User changed location tracking to $isChecked")
model.provideLocation.value = isChecked
checkLocationEnabled(getString(R.string.location_disabled))
model.meshService?.setupProvideLocation()
model.meshService?.startProvideLocation()
}
} else {
model.provideLocation.value = isChecked