Add core data modules (#3169)

This commit is contained in:
Phil Oliver
2025-09-22 23:49:28 -04:00
committed by GitHub
parent bb2e6b9a7d
commit 53fdda3a9c
17 changed files with 213 additions and 193 deletions
+2
View File
@@ -178,6 +178,8 @@ androidComponents {
project.afterEvaluate { logger.lifecycle("Version code is set to: ${android.defaultConfig.versionCode}") } project.afterEvaluate { logger.lifecycle("Version code is set to: ${android.defaultConfig.versionCode}") }
dependencies { dependencies {
implementation(projects.core.data)
implementation(projects.core.datastore)
implementation(projects.core.model) implementation(projects.core.model)
implementation(projects.core.navigation) implementation(projects.core.navigation)
implementation(projects.core.network) implementation(projects.core.network)
@@ -28,8 +28,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddress
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddressesRepository
import com.geeksville.mesh.repository.network.NetworkRepository import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
import com.geeksville.mesh.repository.radio.InterfaceId import com.geeksville.mesh.repository.radio.InterfaceId
@@ -52,6 +50,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.strings.R import org.meshtastic.core.strings.R
import javax.inject.Inject import javax.inject.Inject
@@ -107,7 +107,7 @@ constructor(
private val usbManagerLazy: dagger.Lazy<UsbManager>, private val usbManagerLazy: dagger.Lazy<UsbManager>,
private val networkRepository: NetworkRepository, private val networkRepository: NetworkRepository,
private val radioInterfaceService: RadioInterfaceService, private val radioInterfaceService: RadioInterfaceService,
private val recentAddressesRepository: RecentAddressesRepository, private val recentAddressesDataSource: RecentAddressesDataSource,
) : ViewModel(), ) : ViewModel(),
Logging { Logging {
private val context: Context private val context: Context
@@ -125,7 +125,7 @@ constructor(
// Flow for discovered TCP devices, using recent addresses for potential name enrichment // Flow for discovered TCP devices, using recent addresses for potential name enrichment
private val processedDiscoveredTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> = private val processedDiscoveredTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
combine(networkRepository.resolvedList, recentAddressesRepository.recentAddresses) { tcpServices, recentList -> combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) { tcpServices, recentList ->
val recentMap = recentList.associateBy({ it.address }, { it.name }) val recentMap = recentList.associateBy({ it.address }, { it.name })
tcpServices tcpServices
.map { service -> .map { service ->
@@ -149,7 +149,7 @@ constructor(
// Flow for recent TCP devices, filtered to exclude any currently discovered devices // Flow for recent TCP devices, filtered to exclude any currently discovered devices
private val filteredRecentTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> = private val filteredRecentTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
combine(recentAddressesRepository.recentAddresses, processedDiscoveredTcpDevicesFlow) { combine(recentAddressesDataSource.recentAddresses, processedDiscoveredTcpDevicesFlow) {
recentList, recentList,
discoveredDevices, discoveredDevices,
-> ->
@@ -328,11 +328,11 @@ constructor(
fun addRecentAddress(address: String, name: String) { fun addRecentAddress(address: String, name: String) {
if (!address.startsWith("t")) return if (!address.startsWith("t")) return
viewModelScope.launch { recentAddressesRepository.add(RecentAddress(address, name)) } viewModelScope.launch { recentAddressesDataSource.add(RecentAddress(address, name)) }
} }
fun removeRecentAddress(address: String) { fun removeRecentAddress(address: String) {
viewModelScope.launch { recentAddressesRepository.remove(address) } viewModelScope.launch { recentAddressesDataSource.remove(address) }
} }
// Called by the GUI when a new device has been selected by the user // Called by the GUI when a new device has been selected by the user
@@ -45,6 +45,9 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.meshtastic.core.datastore.ChannelSetDataSource
import org.meshtastic.core.datastore.LocalConfigDataSource
import org.meshtastic.core.datastore.ModuleConfigDataSource
import javax.inject.Inject import javax.inject.Inject
/** /**
@@ -56,9 +59,9 @@ class RadioConfigRepository
constructor( constructor(
private val serviceRepository: ServiceRepository, private val serviceRepository: ServiceRepository,
private val nodeDB: NodeRepository, private val nodeDB: NodeRepository,
private val channelSetRepository: ChannelSetRepository, private val channelSetDataSource: ChannelSetDataSource,
private val localConfigRepository: LocalConfigRepository, private val localConfigDataSource: LocalConfigDataSource,
private val moduleConfigRepository: ModuleConfigRepository, private val moduleConfigDataSource: ModuleConfigDataSource,
) { ) {
val meshService: IMeshService? val meshService: IMeshService?
get() = serviceRepository.meshService get() = serviceRepository.meshService
@@ -104,17 +107,17 @@ constructor(
} }
/** Flow representing the [ChannelSet] data store. */ /** Flow representing the [ChannelSet] data store. */
val channelSetFlow: Flow<ChannelSet> = channelSetRepository.channelSetFlow val channelSetFlow: Flow<ChannelSet> = channelSetDataSource.channelSetFlow
/** Clears the [ChannelSet] data in the data store. */ /** Clears the [ChannelSet] data in the data store. */
suspend fun clearChannelSet() { suspend fun clearChannelSet() {
channelSetRepository.clearChannelSet() channelSetDataSource.clearChannelSet()
} }
/** Replaces the [ChannelSettings] list with a new [settingsList]. */ /** Replaces the [ChannelSettings] list with a new [settingsList]. */
suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) { suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
channelSetRepository.clearSettings() channelSetDataSource.clearSettings()
channelSetRepository.addAllSettings(settingsList) channelSetDataSource.addAllSettings(settingsList)
} }
/** /**
@@ -124,14 +127,14 @@ constructor(
* @param channel The [Channel] provided. * @param channel The [Channel] provided.
* @return the index of the admin channel after the update (if not found, returns 0). * @return the index of the admin channel after the update (if not found, returns 0).
*/ */
suspend fun updateChannelSettings(channel: Channel) = channelSetRepository.updateChannelSettings(channel) suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel)
/** Flow representing the [LocalConfig] data store. */ /** Flow representing the [LocalConfig] data store. */
val localConfigFlow: Flow<LocalConfig> = localConfigRepository.localConfigFlow val localConfigFlow: Flow<LocalConfig> = localConfigDataSource.localConfigFlow
/** Clears the [LocalConfig] data in the data store. */ /** Clears the [LocalConfig] data in the data store. */
suspend fun clearLocalConfig() { suspend fun clearLocalConfig() {
localConfigRepository.clearLocalConfig() localConfigDataSource.clearLocalConfig()
} }
/** /**
@@ -140,16 +143,16 @@ constructor(
* @param config The [Config] to be set. * @param config The [Config] to be set.
*/ */
suspend fun setLocalConfig(config: Config) { suspend fun setLocalConfig(config: Config) {
localConfigRepository.setLocalConfig(config) localConfigDataSource.setLocalConfig(config)
if (config.hasLora()) channelSetRepository.setLoraConfig(config.lora) if (config.hasLora()) channelSetDataSource.setLoraConfig(config.lora)
} }
/** Flow representing the [LocalModuleConfig] data store. */ /** Flow representing the [LocalModuleConfig] data store. */
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigRepository.moduleConfigFlow val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigDataSource.moduleConfigFlow
/** Clears the [LocalModuleConfig] data in the data store. */ /** Clears the [LocalModuleConfig] data in the data store. */
suspend fun clearLocalModuleConfig() { suspend fun clearLocalModuleConfig() {
moduleConfigRepository.clearLocalModuleConfig() moduleConfigDataSource.clearLocalModuleConfig()
} }
/** /**
@@ -158,7 +161,7 @@ constructor(
* @param config The [ModuleConfig] to be set. * @param config The [ModuleConfig] to be set.
*/ */
suspend fun setLocalModuleConfig(config: ModuleConfig) { suspend fun setLocalModuleConfig(config: ModuleConfig) {
moduleConfigRepository.setLocalModuleConfig(config) moduleConfigDataSource.setLocalModuleConfig(config)
} }
/** Flow representing the combined [DeviceProfile] protobuf. */ /** Flow representing the combined [DeviceProfile] protobuf. */
@@ -44,15 +44,14 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager import javax.net.ssl.TrustManager
@Singleton @Singleton
class MQTTRepository @Inject constructor( class MQTTRepository @Inject constructor(private val radioConfigRepository: RadioConfigRepository) : Logging {
private val radioConfigRepository: RadioConfigRepository,
) : Logging {
companion object { companion object {
/** /**
* Quality of Service (QoS) levels in MQTT: * Quality of Service (QoS) levels in MQTT:
* - QoS 0: "at most once". Packets are sent once without validation if it has been received. * - QoS 0: "at most once". Packets are sent once without validation if it has been received.
* - QoS 1: "at least once". Packets are sent and stored until the client receives confirmation from the server. MQTT ensures delivery, but duplicates may occur. * - QoS 1: "at least once". Packets are sent and stored until the client receives confirmation from the server.
* MQTT ensures delivery, but duplicates may occur.
* - QoS 2: "exactly once". Similar to QoS 1, but with no duplicates. * - QoS 2: "exactly once". Similar to QoS 1, but with no duplicates.
*/ */
private const val DEFAULT_QOS = 1 private const val DEFAULT_QOS = 1
@@ -84,63 +83,72 @@ class MQTTRepository @Inject constructor(
val rootTopic = mqttConfig.root.ifEmpty { DEFAULT_TOPIC_ROOT } val rootTopic = mqttConfig.root.ifEmpty { DEFAULT_TOPIC_ROOT }
val connectOptions = MqttConnectOptions().apply { val connectOptions =
userName = mqttConfig.username MqttConnectOptions().apply {
password = mqttConfig.password.toCharArray() userName = mqttConfig.username
isAutomaticReconnect = true password = mqttConfig.password.toCharArray()
if (mqttConfig.tlsEnabled) { isAutomaticReconnect = true
socketFactory = sslContext.socketFactory if (mqttConfig.tlsEnabled) {
} socketFactory = sslContext.socketFactory
}
val bufferOptions = DisconnectedBufferOptions().apply {
isBufferEnabled = true
bufferSize = 512
isPersistBuffer = false
isDeleteOldestMessages = true
}
val callback = object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
info("MQTT connectComplete: $serverURI reconnect: $reconnect")
channelSet.subscribeList.ifEmpty { return }.forEach { globalId ->
subscribe("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+")
if (mqttConfig.jsonEnabled) subscribe("$rootTopic$JSON_TOPIC_LEVEL$globalId/+")
} }
subscribe("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+")
} }
override fun connectionLost(cause: Throwable) { val bufferOptions =
info("MQTT connectionLost cause: $cause") DisconnectedBufferOptions().apply {
if (cause is IllegalArgumentException) close(cause) isBufferEnabled = true
bufferSize = 512
isPersistBuffer = false
isDeleteOldestMessages = true
} }
override fun messageArrived(topic: String, message: MqttMessage) { val callback =
trySend(mqttClientProxyMessage { object : MqttCallbackExtended {
this.topic = topic override fun connectComplete(reconnect: Boolean, serverURI: String) {
data = ByteString.copyFrom(message.payload) info("MQTT connectComplete: $serverURI reconnect: $reconnect")
retained = message.isRetained channelSet.subscribeList
}) .ifEmpty {
} return
}
.forEach { globalId ->
subscribe("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+")
if (mqttConfig.jsonEnabled) subscribe("$rootTopic$JSON_TOPIC_LEVEL$globalId/+")
}
subscribe("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+")
}
override fun deliveryComplete(token: IMqttDeliveryToken?) { override fun connectionLost(cause: Throwable) {
info("MQTT deliveryComplete messageId: ${token?.messageId}") info("MQTT connectionLost cause: $cause")
if (cause is IllegalArgumentException) close(cause)
}
override fun messageArrived(topic: String, message: MqttMessage) {
trySend(
mqttClientProxyMessage {
this.topic = topic
data = ByteString.copyFrom(message.payload)
retained = message.isRetained
},
)
}
override fun deliveryComplete(token: IMqttDeliveryToken?) {
info("MQTT deliveryComplete messageId: ${token?.messageId}")
}
} }
}
val scheme = if (mqttConfig.tlsEnabled) "ssl" else "tcp" val scheme = if (mqttConfig.tlsEnabled) "ssl" else "tcp"
val (host, port) = mqttConfig.address.ifEmpty { DEFAULT_SERVER_ADDRESS } val (host, port) =
.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) } mqttConfig.address
.ifEmpty { DEFAULT_SERVER_ADDRESS }
.split(":", limit = 2)
.let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) }
mqttClient = MqttAsyncClient( mqttClient =
URI(scheme, null, host, port, "", "", "").toString(), MqttAsyncClient(URI(scheme, null, host, port, "", "", "").toString(), ownerId, MemoryPersistence()).apply {
ownerId, setCallback(callback)
MemoryPersistence(), setBufferOpts(bufferOptions)
).apply { connect(connectOptions)
setCallback(callback) }
setBufferOpts(bufferOptions)
connect(connectOptions)
}
awaitClose { disconnect() } awaitClose { disconnect() }
} }
+2
View File
@@ -79,6 +79,8 @@ dependencies {
kover(projects.app) kover(projects.app)
kover(projects.meshServiceExample) kover(projects.meshServiceExample)
kover(projects.core.data)
kover(projects.core.datastore)
kover(projects.core.model) kover(projects.core.model)
kover(projects.core.navigation) kover(projects.core.navigation)
kover(projects.core.network) kover(projects.core.network)
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.kover)
}
android { namespace = "org.meshtastic.core.data" }
dependencies {}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.hilt)
alias(libs.plugins.meshtastic.kotlinx.serialization)
alias(libs.plugins.kover)
}
android { namespace = "org.meshtastic.core.datastore" }
dependencies {
implementation(projects.core.proto)
implementation(libs.bundles.datastore)
implementation(libs.kotlinx.serialization.json)
implementation(libs.timber)
}
@@ -15,30 +15,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.ChannelProtos.Channel import com.geeksville.mesh.ChannelProtos.Channel
import com.geeksville.mesh.ChannelProtos.ChannelSettings import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos import com.geeksville.mesh.ConfigProtos
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import timber.log.Timber
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
/** /** Class that handles saving and retrieving [ChannelSet] data. */
* Class that handles saving and retrieving [ChannelSet] data. @Singleton
*/ class ChannelSetDataSource @Inject constructor(private val channelSetStore: DataStore<ChannelSet>) {
class ChannelSetRepository @Inject constructor( val channelSetFlow: Flow<ChannelSet> =
private val channelSetStore: DataStore<ChannelSet> channelSetStore.data.catch { exception ->
) : Logging {
val channelSetFlow: Flow<ChannelSet> = channelSetStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data // dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) { if (exception is IOException) {
errormsg("Error reading DeviceConfig settings: ${exception.message}") Timber.e("Error reading DeviceConfig settings: ${exception.message}")
emit(ChannelSet.getDefaultInstance()) emit(ChannelSet.getDefaultInstance())
} else { } else {
throw exception throw exception
@@ -46,26 +44,18 @@ class ChannelSetRepository @Inject constructor(
} }
suspend fun clearChannelSet() { suspend fun clearChannelSet() {
channelSetStore.updateData { preference -> channelSetStore.updateData { preference -> preference.toBuilder().clear().build() }
preference.toBuilder().clear().build()
}
} }
suspend fun clearSettings() { suspend fun clearSettings() {
channelSetStore.updateData { preference -> channelSetStore.updateData { preference -> preference.toBuilder().clearSettings().build() }
preference.toBuilder().clearSettings().build()
}
} }
suspend fun addAllSettings(settingsList: List<ChannelSettings>) { suspend fun addAllSettings(settingsList: List<ChannelSettings>) {
channelSetStore.updateData { preference -> channelSetStore.updateData { preference -> preference.toBuilder().addAllSettings(settingsList).build() }
preference.toBuilder().addAllSettings(settingsList).build()
}
} }
/** /** Updates the [ChannelSettings] list with the provided channel. */
* Updates the [ChannelSettings] list with the provided channel.
*/
suspend fun updateChannelSettings(channel: Channel) { suspend fun updateChannelSettings(channel: Channel) {
if (channel.role == Channel.Role.DISABLED) return if (channel.role == Channel.Role.DISABLED) return
channelSetStore.updateData { preference -> channelSetStore.updateData { preference ->
@@ -80,8 +70,6 @@ class ChannelSetRepository @Inject constructor(
} }
suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) { suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) {
channelSetStore.updateData { preference -> channelSetStore.updateData { preference -> preference.toBuilder().setLoraConfig(config).build() }
preference.toBuilder().setLoraConfig(config).build()
}
} }
} }
@@ -15,28 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ConfigProtos.Config import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import timber.log.Timber
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
/** /** Class that handles saving and retrieving [LocalConfig] data. */
* Class that handles saving and retrieving [LocalConfig] data. @Singleton
*/ class LocalConfigDataSource @Inject constructor(private val localConfigStore: DataStore<LocalConfig>) {
class LocalConfigRepository @Inject constructor( val localConfigFlow: Flow<LocalConfig> =
private val localConfigStore: DataStore<LocalConfig>, localConfigStore.data.catch { exception ->
) : Logging {
val localConfigFlow: Flow<LocalConfig> = localConfigStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data // dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) { if (exception is IOException) {
errormsg("Error reading LocalConfig settings: ${exception.message}") Timber.e("Error reading LocalConfig settings: ${exception.message}")
emit(LocalConfig.getDefaultInstance()) emit(LocalConfig.getDefaultInstance())
} else { } else {
throw exception throw exception
@@ -44,14 +42,10 @@ class LocalConfigRepository @Inject constructor(
} }
suspend fun clearLocalConfig() { suspend fun clearLocalConfig() {
localConfigStore.updateData { preference -> localConfigStore.updateData { preference -> preference.toBuilder().clear().build() }
preference.toBuilder().clear().build()
}
} }
/** /** Updates [LocalConfig] from each [Config] oneOf. */
* Updates [LocalConfig] from each [Config] oneOf.
*/
suspend fun setLocalConfig(config: Config) = localConfigStore.updateData { suspend fun setLocalConfig(config: Config) = localConfigStore.updateData {
val builder = it.toBuilder() val builder = it.toBuilder()
config.allFields.forEach { (field, value) -> config.allFields.forEach { (field, value) ->
@@ -59,7 +53,7 @@ class LocalConfigRepository @Inject constructor(
if (localField != null) { if (localField != null) {
builder.setField(localField, value) builder.setField(localField, value)
} else { } else {
errormsg("Error writing LocalConfig settings: ${config.payloadVariantCase}") Timber.e("Error writing LocalConfig settings: ${config.payloadVariantCase}")
} }
} }
builder.build() builder.build()
@@ -15,28 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import timber.log.Timber
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
/** /** Class that handles saving and retrieving [LocalModuleConfig] data. */
* Class that handles saving and retrieving [LocalModuleConfig] data. @Singleton
*/ class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore: DataStore<LocalModuleConfig>) {
class ModuleConfigRepository @Inject constructor( val moduleConfigFlow: Flow<LocalModuleConfig> =
private val moduleConfigStore: DataStore<LocalModuleConfig>, moduleConfigStore.data.catch { exception ->
) : Logging {
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data // dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) { if (exception is IOException) {
errormsg("Error reading LocalModuleConfig settings: ${exception.message}") Timber.e("Error reading LocalModuleConfig settings: ${exception.message}")
emit(LocalModuleConfig.getDefaultInstance()) emit(LocalModuleConfig.getDefaultInstance())
} else { } else {
throw exception throw exception
@@ -44,14 +42,10 @@ class ModuleConfigRepository @Inject constructor(
} }
suspend fun clearLocalModuleConfig() { suspend fun clearLocalModuleConfig() {
moduleConfigStore.updateData { preference -> moduleConfigStore.updateData { preference -> preference.toBuilder().clear().build() }
preference.toBuilder().clear().build()
}
} }
/** /** Updates [LocalModuleConfig] from each [ModuleConfig] oneOf. */
* Updates [LocalModuleConfig] from each [ModuleConfig] oneOf.
*/
suspend fun setLocalModuleConfig(config: ModuleConfig) = moduleConfigStore.updateData { suspend fun setLocalModuleConfig(config: ModuleConfig) = moduleConfigStore.updateData {
val builder = it.toBuilder() val builder = it.toBuilder()
config.allFields.forEach { (field, value) -> config.allFields.forEach { (field, value) ->
@@ -59,7 +53,7 @@ class ModuleConfigRepository @Inject constructor(
if (localField != null) { if (localField != null) {
builder.setField(localField, value) builder.setField(localField, value)
} else { } else {
errormsg("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}") Timber.e("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}")
} }
} }
builder.build() builder.build()
@@ -15,15 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore.recentaddresses package org.meshtastic.core.datastore
import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import com.geeksville.mesh.android.Logging
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -31,17 +28,13 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.meshtastic.core.strings.R import org.meshtastic.core.datastore.model.RecentAddress
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RecentAddressesRepository class RecentAddressesDataSource @Inject constructor(private val dataStore: DataStore<Preferences>) {
@Inject
constructor(
@ApplicationContext private val context: Context,
private val dataStore: DataStore<Preferences>,
) : Logging {
private object PreferencesKeys { private object PreferencesKeys {
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses") val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
} }
@@ -53,11 +46,11 @@ constructor(
try { try {
Json.decodeFromString<List<RecentAddress>>(jsonString) Json.decodeFromString<List<RecentAddress>>(jsonString)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
warn("Could not parse recent addresses, falling back to legacy parsing: ${e.message}") Timber.w("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
// Fallback to legacy parsing // Fallback to legacy parsing
parseLegacyRecentAddresses(jsonString) parseLegacyRecentAddresses(jsonString)
} catch (e: SerializationException) { } catch (e: SerializationException) {
warn("Could not parse recent addresses, falling back to legacy parsing: ${e.message}") Timber.w("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
// Fallback to legacy parsing // Fallback to legacy parsing
parseLegacyRecentAddresses(jsonString) parseLegacyRecentAddresses(jsonString)
} }
@@ -76,11 +69,11 @@ constructor(
} }
is String -> { is String -> {
// Old format: just the address string // Old format: just the address string
RecentAddress(address = item, name = context.getString(R.string.meshtastic)) RecentAddress(address = item, name = "Meshtastic")
} }
else -> { else -> {
// Unknown format, log or handle as an error if necessary // Unknown format, log or handle as an error if necessary
warn("Unknown item type in recent IP addresses: $item") Timber.w("Unknown item type in recent IP addresses: $item")
null null
} }
} }
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore.di
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
@@ -30,16 +30,18 @@ import androidx.datastore.preferences.preferencesDataStoreFile
import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddressesRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
import javax.inject.Singleton
private const val USER_PREFERENCES_NAME = "user_preferences" private const val USER_PREFERENCES_NAME = "user_preferences"
@@ -48,12 +50,9 @@ private const val USER_PREFERENCES_NAME = "user_preferences"
object DataStoreModule { object DataStoreModule {
@Singleton @Singleton
@Provides @Provides
fun providePreferencesDataStore( fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> =
@ApplicationContext appContext: Context
): DataStore<Preferences> =
PreferenceDataStoreFactory.create( PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
migrations = listOf(SharedPreferencesMigration(appContext, USER_PREFERENCES_NAME)), migrations = listOf(SharedPreferencesMigration(appContext, USER_PREFERENCES_NAME)),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) }, produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
@@ -61,36 +60,22 @@ object DataStoreModule {
@Singleton @Singleton
@Provides @Provides
fun provideRecentAddressesRepository( fun provideLocalConfigDataStore(@ApplicationContext appContext: Context): DataStore<LocalConfig> =
@ApplicationContext context: Context,
dataStore: DataStore<Preferences>,
): RecentAddressesRepository = RecentAddressesRepository(context, dataStore)
@Singleton
@Provides
fun provideLocalConfigDataStore(
@ApplicationContext appContext: Context
): DataStore<LocalConfig> =
DataStoreFactory.create( DataStoreFactory.create(
serializer = LocalConfigSerializer, serializer = LocalConfigSerializer,
produceFile = { appContext.dataStoreFile("local_config.pb") }, produceFile = { appContext.dataStoreFile("local_config.pb") },
corruptionHandler = corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig.getDefaultInstance() }),
ReplaceFileCorruptionHandler(produceNewData = { LocalConfig.getDefaultInstance() }),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
) )
@Singleton @Singleton
@Provides @Provides
fun provideModuleConfigDataStore( fun provideModuleConfigDataStore(@ApplicationContext appContext: Context): DataStore<LocalModuleConfig> =
@ApplicationContext appContext: Context
): DataStore<LocalModuleConfig> =
DataStoreFactory.create( DataStoreFactory.create(
serializer = ModuleConfigSerializer, serializer = ModuleConfigSerializer,
produceFile = { appContext.dataStoreFile("module_config.pb") }, produceFile = { appContext.dataStoreFile("module_config.pb") },
corruptionHandler = corruptionHandler =
ReplaceFileCorruptionHandler( ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig.getDefaultInstance() }),
produceNewData = { LocalModuleConfig.getDefaultInstance() }
),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
) )
@@ -100,8 +85,7 @@ object DataStoreModule {
DataStoreFactory.create( DataStoreFactory.create(
serializer = ChannelSetSerializer, serializer = ChannelSetSerializer,
produceFile = { appContext.dataStoreFile("channel_set.pb") }, produceFile = { appContext.dataStoreFile("channel_set.pb") },
corruptionHandler = corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet.getDefaultInstance() }),
ReplaceFileCorruptionHandler(produceNewData = { ChannelSet.getDefaultInstance() }),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
) )
} }
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore.recentaddresses package org.meshtastic.core.datastore.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore.serializer
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
@@ -24,9 +24,7 @@ import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
/** /** Serializer for the [ChannelSet] object defined in apponly.proto. */
* Serializer for the [ChannelSet] object defined in apponly.proto.
*/
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
object ChannelSetSerializer : Serializer<ChannelSet> { object ChannelSetSerializer : Serializer<ChannelSet> {
override val defaultValue: ChannelSet = ChannelSet.getDefaultInstance() override val defaultValue: ChannelSet = ChannelSet.getDefaultInstance()
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore.serializer
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
@@ -24,9 +24,7 @@ import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
/** /** Serializer for the [LocalConfig] object defined in localonly.proto. */
* Serializer for the [LocalConfig] object defined in localonly.proto.
*/
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
object LocalConfigSerializer : Serializer<LocalConfig> { object LocalConfigSerializer : Serializer<LocalConfig> {
override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance() override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance()
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.geeksville.mesh.repository.datastore package org.meshtastic.core.datastore.serializer
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
@@ -24,9 +24,7 @@ import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
/** /** Serializer for the [LocalModuleConfig] object defined in localonly.proto. */
* Serializer for the [LocalModuleConfig] object defined in localonly.proto.
*/
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
object ModuleConfigSerializer : Serializer<LocalModuleConfig> { object ModuleConfigSerializer : Serializer<LocalModuleConfig> {
override val defaultValue: LocalModuleConfig = LocalModuleConfig.getDefaultInstance() override val defaultValue: LocalModuleConfig = LocalModuleConfig.getDefaultInstance()
+1 -1
View File
@@ -17,7 +17,7 @@ import org.gradle.kotlin.dsl.maven
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
include(":app", ":core:model", ":core:navigation", ":core:network", ":core:prefs", ":core:proto", include(":app", ":core:data", ":core:datastore", ":core:model", ":core:navigation", ":core:network", ":core:prefs", ":core:proto",
":core:strings", ":feature:map", ":mesh_service_example") ":core:strings", ":feature:map", ":mesh_service_example")
rootProject.name = "MeshtasticAndroid" rootProject.name = "MeshtasticAndroid"