From 2ed5548abb93502635d2ca0c7dadd5e86c5c0176 Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 12 Sep 2022 19:07:30 -0300 Subject: [PATCH] create ChannelSet datastore --- .../java/com/geeksville/mesh/MainActivity.kt | 9 +-- .../java/com/geeksville/mesh/model/UIState.kt | 23 +++---- .../datastore/ChannelSetRepository.kt | 63 +++++++++++++++++++ .../datastore/ChannelSetSerializer.kt | 26 ++++++++ .../repository/datastore/DataStoreModule.kt | 14 +++++ .../datastore/LocalConfigRepository.kt | 4 +- .../datastore/LocalConfigSerializer.kt | 1 + .../geeksville/mesh/service/MeshService.kt | 61 +++++++----------- .../com/geeksville/mesh/ui/ChannelFragment.kt | 11 ++-- .../geeksville/mesh/ui/ContactsFragment.kt | 5 +- .../geeksville/mesh/ui/SettingsFragment.kt | 6 +- 11 files changed, 154 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 4504105b..59863fd7 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -473,14 +473,7 @@ class MainActivity : BaseActivity(), Logging { // 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.setChannels( - ChannelSet( - AppOnlyProtos.ChannelSet.parseFrom( - service.channels - ) - ) - ) + // model.setChannels(ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels))) model.updateNodesFromDevice() 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 df50109e..cbc9ce06 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -19,6 +19,7 @@ import com.geeksville.mesh.database.QuickChatActionRepository import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.LocalOnlyProtos.LocalConfig +import com.geeksville.mesh.repository.datastore.ChannelSetRepository import com.geeksville.mesh.repository.datastore.LocalConfigRepository import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.util.GPSFormat @@ -65,6 +66,7 @@ fun getInitials(nameIn: String): String { class UIViewModel @Inject constructor( private val app: Application, private val packetRepository: PacketRepository, + private val channelSetRepository: ChannelSetRepository, private val localConfigRepository: LocalConfigRepository, private val quickChatActionRepository: QuickChatActionRepository, private val preferences: SharedPreferences @@ -77,6 +79,9 @@ class UIViewModel @Inject constructor( val localConfig: StateFlow = _localConfig val config get() = _localConfig.value + private val _channels = MutableStateFlow(ChannelSet()) + val channels: StateFlow = _channels + private val _quickChatActions = MutableStateFlow>(emptyList()) val quickChatActions: StateFlow> = _quickChatActions @@ -96,6 +101,11 @@ class UIViewModel @Inject constructor( _quickChatActions.value = actions } } + viewModelScope.launch { + channelSetRepository.channelSetFlow.collect { channelSet -> + _channels.value = ChannelSet(channelSet) + } + } debug("ViewModel created") } @@ -125,9 +135,6 @@ class UIViewModel @Inject constructor( _connectionState.value = connectionState } - private val _channels = MutableLiveData() - val channels: LiveData get() = _channels - private val _requestChannelUrl = MutableLiveData(null) val requestChannelUrl: LiveData get() = _requestChannelUrl @@ -242,20 +249,10 @@ class UIViewModel @Inject constructor( /// Set the radio config (also updates our saved copy in preferences) fun setChannels(c: ChannelSet) { - if (_channels.value == c) return debug("Setting new channels!") meshService?.channels = c.protobuf.toByteArray() - _channels.value = - c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings) - - preferences.edit { - this.putString("channel-url", c.getChannelUrl().toString()) - } } - /// Kinda ugly - created in the activity but used from Compose - figure out if there is a cleaner way GIXME - // lateinit var googleSignInClient: GoogleSignInClient - /// our name in hte radio /// Note, we generate owner initials automatically for now /// our activity will read this from prefs or set it to the empty string diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt new file mode 100644 index 00000000..25438373 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt @@ -0,0 +1,63 @@ +package com.geeksville.mesh.repository.datastore + +import androidx.datastore.core.DataStore +import com.geeksville.mesh.android.Logging +import com.geeksville.mesh.AppOnlyProtos.ChannelSet +import com.geeksville.mesh.ChannelProtos +import com.geeksville.mesh.ConfigProtos +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import java.io.IOException +import javax.inject.Inject + +/** + * Class that handles saving and retrieving channel settings + */ +class ChannelSetRepository @Inject constructor( + private val channelSetStore: DataStore +) : Logging { + val channelSetFlow: Flow = channelSetStore.data + .catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + errormsg("Error reading DeviceConfig settings: ${exception.message}") + emit(ChannelSet.getDefaultInstance()) + } else { + throw exception + } + } + + suspend fun clearChannelSet() { + channelSetStore.updateData { preference -> + preference.toBuilder().clear().build() + } + } + + suspend fun clearSettings() { + channelSetStore.updateData { preference -> + preference.toBuilder().clearSettings().build() + } + } + + suspend fun addSettings(channel: ChannelProtos.Channel) { + channelSetStore.updateData { preference -> + preference.toBuilder().addSettings(channel.index, channel.settings).build() + } + } + + suspend fun addAllSettings(channelSet: ChannelSet) { + channelSetStore.updateData { preference -> + preference.toBuilder().addAllSettings(channelSet.settingsList).build() + } + } + + suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) { + channelSetStore.updateData { preference -> + preference.toBuilder().setLoraConfig(config).build() + } + } + + suspend fun fetchInitialChannelSet() = channelSetStore.data.first() + +} diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt new file mode 100644 index 00000000..89cfcbc3 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt @@ -0,0 +1,26 @@ +package com.geeksville.mesh.repository.datastore + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.geeksville.mesh.AppOnlyProtos.ChannelSet +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream + +/** + * Serializer for the [ChannelSet] object defined in apponly.proto. + */ +@Suppress("BlockingMethodInNonBlockingContext") +object ChannelSetSerializer : Serializer { + override val defaultValue: ChannelSet = ChannelSet.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): ChannelSet { + try { + return ChannelSet.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo(t: ChannelSet, output: OutputStream) = t.writeTo(output) +} 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 index 5f8805d6..069f2617 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt @@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile +import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import dagger.Module import dagger.Provides @@ -32,4 +33,17 @@ object DataStoreModule { scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) ) } + + @Singleton + @Provides + fun provideChannelSetDataStore(@ApplicationContext appContext: Context): DataStore { + return DataStoreFactory.create( + serializer = ChannelSetSerializer, + produceFile = { appContext.dataStoreFile("channel_set.pb") }, + corruptionHandler = ReplaceFileCorruptionHandler( + produceNewData = { ChannelSet.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 index 4553a59c..b98abbe4 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt @@ -16,7 +16,8 @@ import javax.inject.Inject * Class that handles saving and retrieving config settings */ class LocalConfigRepository @Inject constructor( - private val localConfigStore: DataStore + private val localConfigStore: DataStore, + private val channelSetRepository: ChannelSetRepository, ) : Logging { val localConfigFlow: Flow = localConfigStore.data .catch { exception -> @@ -98,6 +99,7 @@ class LocalConfigRepository @Inject constructor( localConfigStore.updateData { preference -> preference.toBuilder().setLora(config).build() } + channelSetRepository.setLoraConfig(config) } private suspend fun setBluetoothConfig(config: ConfigProtos.Config.BluetoothConfig) { 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 index 749c3357..f210493d 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt @@ -10,6 +10,7 @@ import java.io.OutputStream /** * Serializer for the [LocalConfig] object defined in localonly.proto. */ +@Suppress("BlockingMethodInNonBlockingContext") object LocalConfigSerializer : Serializer { override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance() 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 b1de36a7..df9a0eaf 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -11,12 +11,14 @@ import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.* +import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.ToRadio 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.ChannelSetRepository import com.geeksville.mesh.repository.datastore.LocalConfigRepository import com.geeksville.mesh.repository.location.LocationRepository import com.geeksville.mesh.repository.radio.BluetoothInterface @@ -34,7 +36,6 @@ import kotlinx.serialization.json.Json import java.util.* import javax.inject.Inject import kotlin.math.absoluteValue -import kotlin.math.max /** * Handles all the communication with android apps. Also keeps an internal model @@ -60,6 +61,9 @@ class MeshService : Service(), Logging { @Inject lateinit var localConfigRepository: LocalConfigRepository + @Inject + lateinit var channelSetRepository: ChannelSetRepository + companion object : Logging { /// Intents broadcast by MeshService @@ -350,10 +354,7 @@ class MeshService : Service(), Logging { var myNodeInfo: MyNodeInfo? = null - private var localConfig: LocalOnlyProtos.LocalConfig = - LocalOnlyProtos.LocalConfig.newBuilder().build() - - private var channels = fixupChannelList(listOf()) + private var localConfig: LocalConfig = LocalConfig.getDefaultInstance() /// True after we've done our initial node db init @Volatile @@ -467,16 +468,8 @@ class MeshService : Service(), Logging { /// Convert the channels array into a ChannelSet private var channelSet: AppOnlyProtos.ChannelSet get() { - val cs = channels.filter { - it.role != ChannelProtos.Channel.Role.DISABLED - }.map { - it.settings - } - - return AppOnlyProtos.ChannelSet.newBuilder().apply { - addAllSettings(cs) - loraConfig = localConfig.lora - }.build() + // this is never called + return AppOnlyProtos.ChannelSet.getDefaultInstance() } set(value) { val asChannels = value.settingsList.mapIndexed { i, c -> @@ -493,10 +486,13 @@ class MeshService : Service(), Logging { setChannel(it) } + serviceScope.handledLaunch { + channelSetRepository.clearSettings() + channelSetRepository.addAllSettings(value) + } + val newConfig = config { lora = value.loraConfig } if (localConfig.lora != newConfig.lora) sendDeviceConfig(newConfig) - - channels = fixupChannelList(asChannels) } /// Generate a new mesh packet builder with our node as the sender, and the specified node num @@ -742,8 +738,6 @@ class MeshService : Service(), Logging { val mi = myNodeInfo if (mi != null) { val ch = a.getChannelResponse - // add new entries if needed - channels[ch.index] = ch debug("Admin: Received channel ${ch.index}") val packetToSave = Packet( @@ -758,7 +752,8 @@ class MeshService : Service(), Logging { // Stop once we get to the first disabled entry if (/* ch.hasSettings() || */ ch.role != ChannelProtos.Channel.Role.DISABLED) { - // Not done yet, request next channel + // Not done yet, add new entries and request next channel + addChannelSettings(ch) requestChannel(ch.index + 1) } else { debug("We've received the last channel, allowing rest of app to start...") @@ -955,6 +950,12 @@ class MeshService : Service(), Logging { } } + private fun addChannelSettings(channel: ChannelProtos.Channel) { + serviceScope.handledLaunch { + channelSetRepository.addSettings(channel) + } + } + private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt() @@ -1284,24 +1285,10 @@ class MeshService : Service(), Logging { regenMyNodeInfo() // We'll need to get a new set of channels and settings now - clearLocalConfig() - - // prefill the channel array with null channels - channels = fixupChannelList(listOf()) - } - - /// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long - private fun fixupChannelList(lIn: List): Array { - // When updating old firmware, we will briefly be told that there is zero channels - val maxChannels = - max(myNodeInfo?.maxChannels ?: 8, 8) // If we don't have my node info, assume 8 channels (source: apponly.options) - val l = lIn.toMutableList() - while (l.size < maxChannels) { - val b = ChannelProtos.Channel.newBuilder() - b.index = l.size - l += b.build() + serviceScope.handledLaunch { + channelSetRepository.clearChannelSet() + localConfigRepository.clearLocalConfig() } - return l.toTypedArray() } /// If we've received our initial config, our radio settings and all of our channels, send any queued packets and broadcast connected to clients diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 666b14fa..f8ce808d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -15,6 +15,7 @@ import android.widget.ArrayAdapter import android.widget.ImageView import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.activityViewModels +import androidx.lifecycle.asLiveData import com.geeksville.mesh.analytics.DataPair import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging @@ -90,7 +91,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { /// Pull the latest data from the model (discarding any user edits) private fun setGUIfromModel() { val channels = model.channels.value - val channel = channels?.primaryChannel + val channel = channels.primaryChannel val connected = model.isConnected() // Only let buttons work if we are connected to the radio @@ -137,7 +138,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } private fun shareChannel() { - model.channels.value?.let { channels -> + model.channels.value.let { channels -> GeeksvilleApplication.analytics.track( "share", @@ -270,7 +271,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { val checked = binding.editableCheckbox.isChecked if (checked) { // User just unlocked for editing - remove the # goo around the channel name - model.channels.value?.primaryChannel?.let { ch -> + model.channels.value.primaryChannel?.let { ch -> // Note: We are careful to show the empty string here if the user was on a default channel, so the user knows they should it for any changes originalName = ch.settings.name binding.channelNameEdit.setText(originalName) @@ -278,7 +279,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } else { // User just locked it, we should warn and then apply changes to radio - model.channels.value?.primaryChannel?.let { oldPrimary -> + model.channels.value.primaryChannel?.let { oldPrimary -> var newSettings = oldPrimary.settings.toBuilder() val newName = binding.channelNameEdit.text.toString().trim() @@ -343,7 +344,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { shareChannel() } - model.channels.observe(viewLifecycleOwner) { + model.channels.asLiveData().observe(viewLifecycleOwner) { setGUIfromModel() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt index d942a476..6883358f 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt @@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.asLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.geeksville.mesh.android.Logging @@ -89,7 +90,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { //grab channel names from DeviceConfig val channels = model.channels.value - val primaryChannel = channels?.primaryChannel + val primaryChannel = channels.primaryChannel val shortName = node?.user?.shortName ?: "???" val longName = @@ -200,7 +201,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { binding.contactsView.adapter = contactsAdapter binding.contactsView.layoutManager = LinearLayoutManager(requireContext()) - model.channels.observe(viewLifecycleOwner) { + model.channels.asLiveData().observe(viewLifecycleOwner) { contactsAdapter.onChannelsChanged() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index e78fe884..ad4b031d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -295,10 +295,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } else updateNodeInfo() } - model.channels.observe(viewLifecycleOwner) { + model.channels.asLiveData().observe(viewLifecycleOwner) { if (!model.isConnected()) { - val channelCount = it?.protobuf?.settingsCount ?: 0 - binding.scanStatusText.text = "Channels ($channelCount / 8)" + val channelCount = it.protobuf.settingsCount + if (channelCount > 0) binding.scanStatusText.text = "Channels ($channelCount / 8)" } }