kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
create ChannelSet datastore
rodzic
382535da47
commit
2ed5548abb
|
@ -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()
|
||||
|
||||
|
|
|
@ -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> = _localConfig
|
||||
val config get() = _localConfig.value
|
||||
|
||||
private val _channels = MutableStateFlow(ChannelSet())
|
||||
val channels: StateFlow<ChannelSet> = _channels
|
||||
|
||||
private val _quickChatActions = MutableStateFlow<List<QuickChatAction>>(emptyList())
|
||||
val quickChatActions: StateFlow<List<QuickChatAction>> = _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<ChannelSet?>()
|
||||
val channels: LiveData<ChannelSet?> get() = _channels
|
||||
|
||||
private val _requestChannelUrl = MutableLiveData<Uri?>(null)
|
||||
val requestChannelUrl: LiveData<Uri?> 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
|
||||
|
|
|
@ -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<ChannelSet>
|
||||
) : Logging {
|
||||
val channelSetFlow: Flow<ChannelSet> = 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()
|
||||
|
||||
}
|
|
@ -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<ChannelSet> {
|
||||
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)
|
||||
}
|
|
@ -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<ChannelSet> {
|
||||
return DataStoreFactory.create(
|
||||
serializer = ChannelSetSerializer,
|
||||
produceFile = { appContext.dataStoreFile("channel_set.pb") },
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(
|
||||
produceNewData = { ChannelSet.getDefaultInstance() }
|
||||
),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ import javax.inject.Inject
|
|||
* Class that handles saving and retrieving config settings
|
||||
*/
|
||||
class LocalConfigRepository @Inject constructor(
|
||||
private val localConfigStore: DataStore<LocalConfig>
|
||||
private val localConfigStore: DataStore<LocalConfig>,
|
||||
private val channelSetRepository: ChannelSetRepository,
|
||||
) : Logging {
|
||||
val localConfigFlow: Flow<LocalConfig> = 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) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import java.io.OutputStream
|
|||
/**
|
||||
* Serializer for the [LocalConfig] object defined in localonly.proto.
|
||||
*/
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object LocalConfigSerializer : Serializer<LocalConfig> {
|
||||
override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance()
|
||||
|
||||
|
|
|
@ -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<ChannelProtos.Channel>())
|
||||
}
|
||||
|
||||
/// scan the channel list and make sure it has one PRIMARY channel and is maxChannels long
|
||||
private fun fixupChannelList(lIn: List<ChannelProtos.Channel>): Array<ChannelProtos.Channel> {
|
||||
// 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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue