refactor: Move "provide location" preference to DataStore (#1877)

This commit is contained in:
James Rich
2025-05-19 16:23:29 -05:00
committed by GitHub
parent de49f6a48b
commit 38b9515fca
7 changed files with 83 additions and 37 deletions

View File

@@ -276,6 +276,8 @@ class MainActivity : AppCompatActivity(), Logging {
// if provideLocation enabled: Start providing location (from phone GPS) to mesh
if (model.provideLocation.value == true) {
service.startProvideLocation()
} else {
service.stopProvideLocation()
}
}
checkNotificationPermissions()

View File

@@ -26,7 +26,6 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.material3.SnackbarHostState
import androidx.core.content.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
@@ -236,7 +235,6 @@ 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()
@@ -608,16 +606,22 @@ class UIViewModel @Inject constructor(
if (config.lora != newConfig.lora) setConfig(newConfig)
}
val provideLocation =
object : MutableLiveData<Boolean>(preferences.getBoolean("provide-location", false)) {
override fun setValue(value: Boolean) {
super.setValue(value)
preferences.edit {
this.putBoolean("provide-location", value)
}
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) {
viewModelScope.launch {
locationRepository.updateLocationPreferences(provideLocation)
if (provideLocation) {
meshService?.startProvideLocation()
} else {
meshService?.stopProvideLocation()
}
}
}
fun setOwner(name: String) {
val user = ourNodeInfo.value?.user?.copy {

View File

@@ -266,7 +266,9 @@ fun NavGraph(
}
)
) { backStackEntry ->
SettingsScreen {
SettingsScreen(
uIViewModel,
) {
navController.navigate(Route.RadioConfig()) {
popUpTo(Route.Settings) {
inclusive = false

View File

@@ -22,9 +22,13 @@ 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
@@ -77,4 +81,11 @@ object DataStoreModule {
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
)
}
@Singleton
@Provides
fun provideLocationPreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
produceFile = { appContext.preferencesDataStoreFile(LOCATION_PREFERNCES_NAME) }
)
}

View File

@@ -27,14 +27,16 @@ 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
@@ -42,13 +44,22 @@ 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.
*/
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
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)
}
}
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
private fun LocationManager.requestLocationUpdates() = callbackFlow {
@@ -84,7 +95,8 @@ class LocationRepository @Inject constructor(
}
info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")
_receivingLocationUpdates.value = true
// _receivingLocationUpdates.value = true
updateLocationPreferences(true)
GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS
try {
@@ -103,7 +115,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)
@@ -116,3 +128,9 @@ 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"

View File

@@ -86,11 +86,12 @@ 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.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.service.MeshService
import kotlinx.coroutines.delay
fun String?.isIPAddress(): Boolean {
@@ -110,19 +111,21 @@ fun SettingsScreen(
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
onSetRegion: () -> Unit,
) {
val currentRegion = uiViewModel.region
val regionUnset = currentRegion == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
val config by uiViewModel.localConfig.collectAsState()
val currentRegion = config.lora.region
val scrollState = rememberScrollState()
val scanStatusText by scanModel.errorText.observeAsState("")
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 &&
connectionState == MeshService.ConnectionState.CONNECTED
val isGpsDisabled = context.gpsDisabled()
LaunchedEffect(isGpsDisabled) {
@@ -165,10 +168,10 @@ fun SettingsScreen(
contract = ActivityResultContracts.RequestMultiplePermissions(),
onResult = { permissions ->
if (permissions.entries.all { it.value }) {
uiViewModel.provideLocation.value = true
uiViewModel.meshService?.startProvideLocation()
uiViewModel.setProvideLocation(true)
} else {
debug("User denied location permission")
uiViewModel.setProvideLocation(false)
uiViewModel.showSnackbar(context.getString(R.string.why_background_required))
}
bluetoothViewModel.permissionsUpdated()
@@ -200,11 +203,11 @@ fun SettingsScreen(
showScanDialog = true
}
LaunchedEffect(connectionState, regionUnset) {
LaunchedEffect(connectionState) {
when (connectionState) {
MeshService.ConnectionState.CONNECTED ->
// Include region unset warning in status string if applicable
MeshService.ConnectionState.CONNECTED -> {
if (regionUnset) R.string.must_set_region else R.string.connected_to
}
MeshService.ConnectionState.DISCONNECTED -> R.string.not_connected
MeshService.ConnectionState.DEVICE_SLEEP -> R.string.connected_sleeping
@@ -216,6 +219,15 @@ fun SettingsScreen(
scanModel.setErrorText(context.getString(it, firmwareString))
}
}
when (connectionState) {
MeshService.ConnectionState.CONNECTED -> {
if (lastConnection != null && lastConnection?.myNodeNum != info?.myNodeNum) {
uiViewModel.setProvideLocation(false)
}
lastConnection = info
}
else -> {}
}
}
Box(modifier = Modifier.fillMaxSize()) {
Column(
@@ -349,21 +361,17 @@ fun SettingsScreen(
Spacer(modifier = Modifier.height(16.dp))
// Provide Location Checkbox
val checked by uiViewModel.provideLocation.collectAsState()
Row(
modifier = Modifier
.fillMaxWidth()
.toggleable(
value = receivingLocationUpdates,
value = checked,
onValueChange = { checked ->
uiViewModel.provideLocation.value = checked
uiViewModel.setProvideLocation(checked)
if (checked && !context.hasLocationPermission()) {
showLocationRationaleDialog = true // Show the Compose dialog
}
if (checked) {
uiViewModel.meshService?.startProvideLocation()
} else {
uiViewModel.meshService?.stopProvideLocation()
}
},
enabled = !isGpsDisabled
)
@@ -371,7 +379,7 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = receivingLocationUpdates,
checked = checked,
onCheckedChange = null,
enabled = !isGpsDisabled // Disable if GPS is disabled
)

View File

@@ -30,7 +30,7 @@ kotlinx-coroutines-android = "1.10.2"
kotlinx-serialization-json = "1.8.1"
lifecycle = "2.9.0"
material = "1.12.0"
material3 = "1.2.0"
material3 = "1.3.2"
mgrs = "2.1.3"
navigation = "2.9.0"
org-eclipse-paho-client-mqttv3 = "1.2.5"
@@ -58,7 +58,7 @@ awesome-app-rating = { group = "com.suddenh4x.ratingdialog", name = "awesome-app
cardview = { group = "androidx.cardview", name = "cardview", version.ref = "cardview" }
coil = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" }
@@ -71,6 +71,7 @@ core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx
core-location-altitude = { group = "androidx.core", name = "core-location-altitude", version.ref = "core-location-altitude" }
core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "core-splashscreen" }
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emoji2" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
@@ -142,7 +143,7 @@ navigation = ["navigation-compose"]
coroutines = ["kotlinx-coroutines-android", "kotlinx-coroutines-guava"]
# Data Storage
datastore = ["datastore"]
datastore = ["datastore", "datastore-preferences"]
room = ["room-runtime", "room-ktx"]
# Dependency Injection