kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
move location service to repository
rodzic
d50e9e1644
commit
6bda993851
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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?
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue