mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-14 20:08:32 -04:00
fix: per-device location sharing (#2010)
This commit is contained in:
@@ -270,14 +270,6 @@ class MainActivity : AppCompatActivity(), Logging {
|
||||
// Called when we gain/lose a connection to our mesh radio
|
||||
private fun onMeshConnectionChanged(newConnection: MeshService.ConnectionState) {
|
||||
if (newConnection == MeshService.ConnectionState.CONNECTED) {
|
||||
serviceRepository.meshService?.let { service ->
|
||||
// if provideLocation enabled: Start providing location (from phone GPS) to mesh
|
||||
if (model.provideLocation.value == true) {
|
||||
service.startProvideLocation()
|
||||
} else {
|
||||
service.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
checkNotificationPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@ class UIViewModel @Inject constructor(
|
||||
_title.value = title
|
||||
}
|
||||
|
||||
val receivingLocationUpdates: StateFlow<Boolean> get() = locationRepository.receivingLocationUpdates
|
||||
val meshService: IMeshService? get() = radioConfigRepository.meshService
|
||||
|
||||
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
|
||||
@@ -546,7 +547,8 @@ class UIViewModel @Inject constructor(
|
||||
// Connection state to our radio device
|
||||
val connectionState get() = radioConfigRepository.connectionState
|
||||
fun isConnected() = connectionState.value != MeshService.ConnectionState.DISCONNECTED
|
||||
val isConnected = radioConfigRepository.connectionState.map { it == MeshService.ConnectionState.CONNECTED }
|
||||
val isConnected =
|
||||
radioConfigRepository.connectionState.map { it == MeshService.ConnectionState.CONNECTED }
|
||||
|
||||
private val _requestChannelSet = MutableStateFlow<AppOnlyProtos.ChannelSet?>(null)
|
||||
val requestChannelSet: StateFlow<AppOnlyProtos.ChannelSet?> get() = _requestChannelSet
|
||||
@@ -651,16 +653,26 @@ class UIViewModel @Inject constructor(
|
||||
if (config.lora != newConfig.lora) setConfig(newConfig)
|
||||
}
|
||||
|
||||
private val _provideLocation = locationRepository.locationPreferencesFlow.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = false
|
||||
)
|
||||
val provideLocation: StateFlow<Boolean> get() = _provideLocation
|
||||
fun setProvideLocation(provideLocation: Boolean) {
|
||||
fun refreshProvideLocation() {
|
||||
viewModelScope.launch {
|
||||
locationRepository.updateLocationPreferences(provideLocation)
|
||||
if (provideLocation) {
|
||||
setProvideLocation(getProvidePref())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProvidePref(): Boolean {
|
||||
val value = preferences.getBoolean("provide-location-$myNodeNum", false)
|
||||
return value
|
||||
}
|
||||
|
||||
private val _provideLocation =
|
||||
MutableStateFlow(getProvidePref())
|
||||
val provideLocation: StateFlow<Boolean> get() = _provideLocation.asStateFlow()
|
||||
|
||||
fun setProvideLocation(value: Boolean) {
|
||||
viewModelScope.launch {
|
||||
preferences.edit { putBoolean("provide-location-$myNodeNum", value) }
|
||||
_provideLocation.value = value
|
||||
if (value) {
|
||||
meshService?.startProvideLocation()
|
||||
} else {
|
||||
meshService?.stopProvideLocation()
|
||||
|
||||
@@ -22,13 +22,9 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.repository.location.LOCATION_PREFERNCES_NAME
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -81,11 +77,4 @@ object DataStoreModule {
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLocationPreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create(
|
||||
produceFile = { appContext.preferencesDataStoreFile(LOCATION_PREFERNCES_NAME) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,16 +27,14 @@ import androidx.core.location.LocationListenerCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import androidx.core.location.LocationRequestCompat
|
||||
import androidx.core.location.altitude.AltitudeConverterCompat
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
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 kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -44,22 +42,13 @@ import javax.inject.Singleton
|
||||
class LocationRepository @Inject constructor(
|
||||
private val context: Application,
|
||||
private val locationManager: dagger.Lazy<LocationManager>,
|
||||
private val locationPreferencesDataStore: DataStore<Preferences>,
|
||||
) : Logging {
|
||||
|
||||
/**
|
||||
* Status of whether the app is actively subscribed to location changes.
|
||||
*/
|
||||
val locationPreferencesFlow = locationPreferencesDataStore.data.map {
|
||||
it[PreferencesKeys.PROVIDE_LOCATION] == true
|
||||
}
|
||||
|
||||
suspend fun updateLocationPreferences(provideLocation: Boolean) =
|
||||
locationPreferencesDataStore.updateData { preferences ->
|
||||
preferences.toMutablePreferences().apply {
|
||||
set(PreferencesKeys.PROVIDE_LOCATION, provideLocation)
|
||||
}
|
||||
}
|
||||
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
|
||||
|
||||
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
|
||||
private fun LocationManager.requestLocationUpdates() = callbackFlow {
|
||||
@@ -95,8 +84,7 @@ class LocationRepository @Inject constructor(
|
||||
}
|
||||
|
||||
info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
|
||||
// _receivingLocationUpdates.value = true
|
||||
updateLocationPreferences(true)
|
||||
_receivingLocationUpdates.value = true
|
||||
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
|
||||
|
||||
try {
|
||||
@@ -115,7 +103,7 @@ class LocationRepository @Inject constructor(
|
||||
|
||||
awaitClose {
|
||||
info("Stopping location requests")
|
||||
// _receivingLocationUpdates.value = false
|
||||
_receivingLocationUpdates.value = false
|
||||
GeeksvilleApplication.analytics.track("location_stop")
|
||||
|
||||
LocationManagerCompat.removeUpdates(this@requestLocationUpdates, locationListener)
|
||||
@@ -128,9 +116,3 @@ class LocationRepository @Inject constructor(
|
||||
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
|
||||
fun getLocations() = locationManager.get().requestLocationUpdates()
|
||||
}
|
||||
|
||||
private object PreferencesKeys {
|
||||
val PROVIDE_LOCATION = booleanPreferencesKey("provide_location")
|
||||
}
|
||||
|
||||
const val LOCATION_PREFERNCES_NAME = "location_preferences"
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.os.Build
|
||||
import android.util.Patterns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -88,7 +89,6 @@ import com.geeksville.mesh.android.gpsDisabled
|
||||
import com.geeksville.mesh.android.hasLocationPermission
|
||||
import com.geeksville.mesh.android.isGooglePlayAvailable
|
||||
import com.geeksville.mesh.android.permissionMissing
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
|
||||
@@ -135,10 +135,10 @@ fun ConnectionsScreen(
|
||||
val connectionState by uiViewModel.connectionState.collectAsState(MeshService.ConnectionState.DISCONNECTED)
|
||||
val devices by scanModel.devices.observeAsState(emptyMap())
|
||||
val scanning by scanModel.spinner.observeAsState(false)
|
||||
val receivingLocationUpdates by uiViewModel.receivingLocationUpdates.collectAsState(false)
|
||||
val context = LocalContext.current
|
||||
val app = (context.applicationContext as GeeksvilleApplication)
|
||||
val info by uiViewModel.myNodeInfo.collectAsState()
|
||||
var lastConnection: MyNodeEntity? by remember { mutableStateOf(null) }
|
||||
val selectedDevice = scanModel.selectedNotNull
|
||||
val bluetoothEnabled by bluetoothViewModel.enabled.observeAsState()
|
||||
val regionUnset = currentRegion == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET &&
|
||||
@@ -205,9 +205,9 @@ fun ConnectionsScreen(
|
||||
onResult = { permissions ->
|
||||
if (permissions.entries.all { it.value }) {
|
||||
uiViewModel.setProvideLocation(true)
|
||||
uiViewModel.meshService?.startProvideLocation()
|
||||
} else {
|
||||
debug("User denied location permission")
|
||||
uiViewModel.setProvideLocation(false)
|
||||
uiViewModel.showSnackbar(context.getString(R.string.why_background_required))
|
||||
}
|
||||
bluetoothViewModel.permissionsUpdated()
|
||||
@@ -239,7 +239,7 @@ fun ConnectionsScreen(
|
||||
showScanDialog = true
|
||||
}
|
||||
|
||||
LaunchedEffect(connectionState) {
|
||||
LaunchedEffect(connectionState, regionUnset) {
|
||||
when (connectionState) {
|
||||
MeshService.ConnectionState.CONNECTED -> {
|
||||
if (regionUnset) R.string.must_set_region else R.string.connected_to
|
||||
@@ -252,16 +252,6 @@ fun ConnectionsScreen(
|
||||
info?.firmwareString ?: context.getString(R.string.unknown)
|
||||
scanModel.setErrorText(context.getString(it, firmwareString))
|
||||
}
|
||||
when (connectionState) {
|
||||
MeshService.ConnectionState.CONNECTED -> {
|
||||
if (lastConnection != null && lastConnection?.myNodeNum != info?.myNodeNum) {
|
||||
uiViewModel.setProvideLocation(false)
|
||||
}
|
||||
lastConnection = info
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
var showSharedContact by remember { mutableStateOf<Node?>(null) }
|
||||
if (showSharedContact != null) {
|
||||
@@ -287,7 +277,7 @@ fun ConnectionsScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
val isConnected by uiViewModel.isConnected.collectAsStateWithLifecycle(false)
|
||||
val isConnected by uiViewModel.isConnected.collectAsState(false)
|
||||
val ourNode by uiViewModel.ourNodeInfo.collectAsState()
|
||||
if (isConnected) {
|
||||
ourNode?.let { node ->
|
||||
@@ -440,35 +430,39 @@ fun ConnectionsScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Provide Location Checkbox
|
||||
val checked by uiViewModel.provideLocation.collectAsState()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = checked,
|
||||
onValueChange = { checked ->
|
||||
uiViewModel.setProvideLocation(checked)
|
||||
if (checked && !context.hasLocationPermission()) {
|
||||
showLocationRationaleDialog = true // Show the Compose dialog
|
||||
}
|
||||
},
|
||||
enabled = !isGpsDisabled
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = !isGpsDisabled // Disable if GPS is disabled
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.provide_location_to_mesh),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
LaunchedEffect(ourNode) {
|
||||
if (ourNode != null) {
|
||||
uiViewModel.refreshProvideLocation()
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(isConnected) {
|
||||
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = provideLocation,
|
||||
onValueChange = { checked ->
|
||||
uiViewModel.setProvideLocation(checked)
|
||||
},
|
||||
enabled = !isGpsDisabled
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = receivingLocationUpdates,
|
||||
onCheckedChange = null,
|
||||
enabled = !isGpsDisabled // Disable if GPS is disabled
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.provide_location_to_mesh),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Provide Location Checkbox
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@@ -559,7 +553,7 @@ fun ConnectionsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Device Scan Dialog
|
||||
// Compose Device Scan Dialog
|
||||
if (showScanDialog) {
|
||||
Dialog(onDismissRequest = {
|
||||
showScanDialog = false
|
||||
@@ -604,7 +598,7 @@ fun ConnectionsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Compose Location Permission Rationale Dialog
|
||||
// Compose Location Permission Rationale Dialog
|
||||
if (showLocationRationaleDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLocationRationaleDialog = false },
|
||||
@@ -628,7 +622,7 @@ fun ConnectionsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Compose Bluetooth Permission Rationale Dialog
|
||||
// Compose Bluetooth Permission Rationale Dialog
|
||||
if (showBluetoothRationaleDialog) {
|
||||
val bluetoothPermissions = context.getBluetoothPermissions()
|
||||
AlertDialog(
|
||||
@@ -656,7 +650,7 @@ fun ConnectionsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
// Compose Report Bug Dialog
|
||||
// Compose Report Bug Dialog
|
||||
if (showReportBugDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showReportBugDialog = false },
|
||||
|
||||
Reference in New Issue
Block a user