mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-05 05:12:51 -05:00
refactor: Move "provide location" preference to DataStore (#1877)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -266,7 +266,9 @@ fun NavGraph(
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
SettingsScreen {
|
||||
SettingsScreen(
|
||||
uIViewModel,
|
||||
) {
|
||||
navController.navigate(Route.RadioConfig()) {
|
||||
popUpTo(Route.Settings) {
|
||||
inclusive = false
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user