diff --git a/app/build.gradle b/app/build.gradle index 0d1ed87c2..cfc6528c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,6 +136,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation "androidx.room:room-runtime:$room_version" implementation "com.google.dagger:hilt-android:$hilt_version" + implementation "androidx.datastore:datastore:$datastore_version" kapt "androidx.room:room-compiler:$room_version" kapt "com.google.dagger:hilt-compiler:$hilt_version" diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 7ae91600e..264543d09 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -655,7 +655,7 @@ class MainActivity : BaseActivity(), Logging, else { // If our app is too old/new, we probably don't understand the new DeviceConfig messages, so we don't read them until here - model.setLocalConfig(LocalOnlyProtos.LocalConfig.parseFrom(service.deviceConfig)) + // model.setLocalConfig(LocalOnlyProtos.LocalConfig.parseFrom(service.deviceConfig)) model.setChannels(ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 8d4589294..2a347491c 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -15,6 +15,7 @@ import com.geeksville.android.Logging import com.geeksville.mesh.* import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.entity.Packet +import com.geeksville.mesh.repository.datastore.LocalConfigRepository import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.util.positionToMeter import dagger.hilt.android.lifecycle.HiltViewModel @@ -59,6 +60,7 @@ fun getInitials(nameIn: String): String { class UIViewModel @Inject constructor( private val app: Application, private val packetRepository: PacketRepository, + private val localConfigRepository: LocalConfigRepository, private val preferences: SharedPreferences ) : ViewModel(), Logging { @@ -67,8 +69,15 @@ class UIViewModel @Inject constructor( init { viewModelScope.launch { - packetRepository.getAllPackets().collect { packets -> - _allPacketState.value = packets + launch { + packetRepository.getAllPackets().collect { packets -> + _allPacketState.value = packets + } + } + launch(Dispatchers.IO) { + localConfigRepository.localConfigFlow.collect { config -> + _localConfig.postValue(config) + } } } debug("ViewModel created") @@ -238,18 +247,6 @@ class UIViewModel @Inject constructor( private fun setDeviceConfig(config: ConfigProtos.Config) { debug("Setting new radio config!") meshService?.deviceConfig = config.toByteArray() - - // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings) - _localConfig.value?.let { localConfig -> - val builder = localConfig.toBuilder() - if (config.hasDevice()) builder.device = config.device - if (config.hasPosition()) builder.position = config.position - if (config.hasPower()) builder.power = config.power - if (config.hasWifi()) builder.wifi = config.wifi - if (config.hasDisplay()) builder.display = config.display - if (config.hasLora()) builder.lora = config.lora - _localConfig.value = builder.build() - } } fun setLocalConfig(localConfig: LocalOnlyProtos.LocalConfig) { diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt new file mode 100644 index 000000000..5f8805d69 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt @@ -0,0 +1,35 @@ +package com.geeksville.mesh.repository.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.dataStoreFile +import com.geeksville.mesh.LocalOnlyProtos.LocalConfig +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object DataStoreModule { + + @Singleton + @Provides + fun provideLocalConfigDataStore(@ApplicationContext appContext: Context): DataStore { + return DataStoreFactory.create( + serializer = LocalConfigSerializer, + produceFile = { appContext.dataStoreFile("local_config.pb") }, + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { LocalConfig.getDefaultInstance() } + ), + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + ) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt new file mode 100644 index 000000000..972e4dfc0 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt @@ -0,0 +1,104 @@ +package com.geeksville.mesh.repository.datastore + +import androidx.datastore.core.DataStore +import com.geeksville.android.Logging +import com.geeksville.mesh.ConfigProtos +import com.geeksville.mesh.LocalOnlyProtos.LocalConfig +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.asSharedFlow +import java.io.IOException +import javax.inject.Inject + +/** + * Class that handles saving and retrieving config settings + */ +class LocalConfigRepository @Inject constructor( + private val localConfigStore: DataStore +) : Logging { + val localConfigFlow: Flow = localConfigStore.data + .catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + errormsg("Error reading LocalConfig settings: ${exception.message}") + emit(LocalConfig.getDefaultInstance()) + } else { + throw exception + } + } + + private val _sendDeviceConfigFlow = MutableSharedFlow() + val sendDeviceConfigFlow = _sendDeviceConfigFlow.asSharedFlow() + + private suspend fun sendDeviceConfig(config: ConfigProtos.Config) { + debug("Sending device config!") + _sendDeviceConfigFlow.emit(config) + } + + /** + * Update LocalConfig and send ConfigProtos.Config Oneof to the radio + */ + suspend fun setRadioConfig(config: ConfigProtos.Config) { + setLocalConfig(config) + sendDeviceConfig(config) + } + + suspend fun clearLocalConfig() { + localConfigStore.updateData { preference -> + preference.toBuilder().clear().build() + } + } + + /** + * Update LocalConfig from each ConfigProtos.Config Oneof + */ + suspend fun setLocalConfig(config: ConfigProtos.Config) { + if (config.hasDevice()) setDeviceConfig(config.device) + if (config.hasPosition()) setPositionConfig(config.position) + if (config.hasPower()) setPowerConfig(config.power) + if (config.hasWifi()) setWifiConfig(config.wifi) + if (config.hasDisplay()) setDisplayConfig(config.display) + if (config.hasLora()) setLoraConfig(config.lora) + } + + private suspend fun setDeviceConfig(config: ConfigProtos.Config.DeviceConfig) { + localConfigStore.updateData { preference -> + preference.toBuilder().setDevice(config).build() + } + } + + private suspend fun setPositionConfig(config: ConfigProtos.Config.PositionConfig) { + localConfigStore.updateData { preference -> + preference.toBuilder().setPosition(config).build() + } + } + + private suspend fun setPowerConfig(config: ConfigProtos.Config.PowerConfig) { + localConfigStore.updateData { preference -> + preference.toBuilder().setPower(config).build() + } + } + + private suspend fun setWifiConfig(config: ConfigProtos.Config.WiFiConfig) { + localConfigStore.updateData { preference -> + preference.toBuilder().setWifi(config).build() + } + } + + private suspend fun setDisplayConfig(config: ConfigProtos.Config.DisplayConfig) { + localConfigStore.updateData { preference -> + preference.toBuilder().setDisplay(config).build() + } + } + + private suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) { + localConfigStore.updateData { preference -> + preference.toBuilder().setLora(config).build() + } + } + + suspend fun fetchInitialLocalConfig() = localConfigStore.data.first() + +} diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt new file mode 100644 index 000000000..749c3357f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt @@ -0,0 +1,25 @@ +package com.geeksville.mesh.repository.datastore + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.geeksville.mesh.LocalOnlyProtos.LocalConfig +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +/** + * Serializer for the [LocalConfig] object defined in localonly.proto. + */ +object LocalConfigSerializer : Serializer { + override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): LocalConfig { + try { + return LocalConfig.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: LocalConfig, output: OutputStream) = t.writeTo(output) +} diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 61abd47f1..e5d135e5c 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -18,6 +18,7 @@ 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.datastore.LocalConfigRepository import com.geeksville.mesh.repository.location.LocationRepository import com.geeksville.mesh.repository.radio.BluetoothInterface import com.geeksville.mesh.repository.radio.RadioInterfaceService @@ -57,6 +58,9 @@ class MeshService : Service(), Logging { @Inject lateinit var locationRepository: LocationRepository + @Inject + lateinit var localConfigRepository: LocalConfigRepository + companion object : Logging { /// Intents broadcast by MeshService @@ -242,6 +246,11 @@ class MeshService : Service(), Logging { serviceScope.handledLaunch { radioInterfaceService.receivedData.collect(::onReceiveFromRadio) } + serviceScope.handledLaunch { + localConfigRepository.localConfigFlow.collect { config -> + localConfig = config + } + } // the rest of our init will happen once we are in radioConnection.onServiceConnected } @@ -498,7 +507,7 @@ class MeshService : Service(), Logging { newPrefs.regionValue = curRegionValue newConfig.lora = newPrefs.build() - sendDeviceConfig(newConfig.build()) + if (localConfig.lora != newConfig.lora) sendDeviceConfig(newConfig.build()) channels = fixupChannelList(asChannels) } @@ -691,7 +700,7 @@ class MeshService : Service(), Logging { if (u.time == 0 && packet.rxTime != 0) u = u.toBuilder().setTime(packet.rxTime).build() handleReceivedTelemetry(packet.from, u, dataPacket.time) - } + } // Handle new style routing info Portnums.PortNum.ROUTING_APP_VALUE -> { @@ -935,6 +944,12 @@ class MeshService : Service(), Logging { } } + private fun setLocalConfig (config: ConfigProtos.Config) { + serviceScope.handledLaunch { + localConfigRepository.setLocalConfig(config) + } + } + private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt() @@ -1261,7 +1276,9 @@ class MeshService : Service(), Logging { regenMyNodeInfo() // We'll need to get a new set of channels and settings now - localConfig = LocalOnlyProtos.LocalConfig.getDefaultInstance() + serviceScope.handledLaunch { + localConfigRepository.clearLocalConfig() + } // prefill the channel array with null channels channels = fixupChannelList(listOf()) @@ -1476,19 +1493,6 @@ class MeshService : Service(), Logging { setLocalConfig(c) } - /** Set our localConfig - */ - private fun setLocalConfig(config: ConfigProtos.Config) { - val builder = localConfig.toBuilder() - if (config.hasDevice()) builder.device = config.device - if (config.hasPosition()) builder.position = config.position - if (config.hasPower()) builder.power = config.power - if (config.hasWifi()) builder.wifi = config.wifi - if (config.hasDisplay()) builder.display = config.display - if (config.hasLora()) builder.lora = config.lora - localConfig = builder.build() - } - /** * Set our owner with either the new or old API */ diff --git a/build.gradle b/build.gradle index ae4f02e75..e3e8a2210 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { ext.coroutines_version = '1.6.0' ext.room_version = '2.4.2' ext.hilt_version = '2.40.5' + ext.datastore_version = '1.0.0' repositories { google()