mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Add core data modules (#3169)
This commit is contained in:
parent
bb2e6b9a7d
commit
53fdda3a9c
17 changed files with 213 additions and 193 deletions
|
|
@ -178,6 +178,8 @@ androidComponents {
|
|||
project.afterEvaluate { logger.lifecycle("Version code is set to: ${android.defaultConfig.versionCode}") }
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.datastore)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.network)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.android.Logging
|
||||
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.Companion.toAddressString
|
||||
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.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
import org.meshtastic.core.strings.R
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ constructor(
|
|||
private val usbManagerLazy: dagger.Lazy<UsbManager>,
|
||||
private val networkRepository: NetworkRepository,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val recentAddressesRepository: RecentAddressesRepository,
|
||||
private val recentAddressesDataSource: RecentAddressesDataSource,
|
||||
) : ViewModel(),
|
||||
Logging {
|
||||
private val context: Context
|
||||
|
|
@ -125,7 +125,7 @@ constructor(
|
|||
|
||||
// Flow for discovered TCP devices, using recent addresses for potential name enrichment
|
||||
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 })
|
||||
tcpServices
|
||||
.map { service ->
|
||||
|
|
@ -149,7 +149,7 @@ constructor(
|
|||
|
||||
// Flow for recent TCP devices, filtered to exclude any currently discovered devices
|
||||
private val filteredRecentTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
|
||||
combine(recentAddressesRepository.recentAddresses, processedDiscoveredTcpDevicesFlow) {
|
||||
combine(recentAddressesDataSource.recentAddresses, processedDiscoveredTcpDevicesFlow) {
|
||||
recentList,
|
||||
discoveredDevices,
|
||||
->
|
||||
|
|
@ -328,11 +328,11 @@ constructor(
|
|||
|
||||
fun addRecentAddress(address: String, name: String) {
|
||||
if (!address.startsWith("t")) return
|
||||
viewModelScope.launch { recentAddressesRepository.add(RecentAddress(address, name)) }
|
||||
viewModelScope.launch { recentAddressesDataSource.add(RecentAddress(address, name)) }
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
@ -56,9 +59,9 @@ class RadioConfigRepository
|
|||
constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeDB: NodeRepository,
|
||||
private val channelSetRepository: ChannelSetRepository,
|
||||
private val localConfigRepository: LocalConfigRepository,
|
||||
private val moduleConfigRepository: ModuleConfigRepository,
|
||||
private val channelSetDataSource: ChannelSetDataSource,
|
||||
private val localConfigDataSource: LocalConfigDataSource,
|
||||
private val moduleConfigDataSource: ModuleConfigDataSource,
|
||||
) {
|
||||
val meshService: IMeshService?
|
||||
get() = serviceRepository.meshService
|
||||
|
|
@ -104,17 +107,17 @@ constructor(
|
|||
}
|
||||
|
||||
/** 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. */
|
||||
suspend fun clearChannelSet() {
|
||||
channelSetRepository.clearChannelSet()
|
||||
channelSetDataSource.clearChannelSet()
|
||||
}
|
||||
|
||||
/** Replaces the [ChannelSettings] list with a new [settingsList]. */
|
||||
suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
|
||||
channelSetRepository.clearSettings()
|
||||
channelSetRepository.addAllSettings(settingsList)
|
||||
channelSetDataSource.clearSettings()
|
||||
channelSetDataSource.addAllSettings(settingsList)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,14 +127,14 @@ constructor(
|
|||
* @param channel The [Channel] provided.
|
||||
* @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. */
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigRepository.localConfigFlow
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigDataSource.localConfigFlow
|
||||
|
||||
/** Clears the [LocalConfig] data in the data store. */
|
||||
suspend fun clearLocalConfig() {
|
||||
localConfigRepository.clearLocalConfig()
|
||||
localConfigDataSource.clearLocalConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,16 +143,16 @@ constructor(
|
|||
* @param config The [Config] to be set.
|
||||
*/
|
||||
suspend fun setLocalConfig(config: Config) {
|
||||
localConfigRepository.setLocalConfig(config)
|
||||
if (config.hasLora()) channelSetRepository.setLoraConfig(config.lora)
|
||||
localConfigDataSource.setLocalConfig(config)
|
||||
if (config.hasLora()) channelSetDataSource.setLoraConfig(config.lora)
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
suspend fun clearLocalModuleConfig() {
|
||||
moduleConfigRepository.clearLocalModuleConfig()
|
||||
moduleConfigDataSource.clearLocalModuleConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -158,7 +161,7 @@ constructor(
|
|||
* @param config The [ModuleConfig] to be set.
|
||||
*/
|
||||
suspend fun setLocalModuleConfig(config: ModuleConfig) {
|
||||
moduleConfigRepository.setLocalModuleConfig(config)
|
||||
moduleConfigDataSource.setLocalModuleConfig(config)
|
||||
}
|
||||
|
||||
/** Flow representing the combined [DeviceProfile] protobuf. */
|
||||
|
|
|
|||
|
|
@ -44,15 +44,14 @@ import javax.net.ssl.SSLContext
|
|||
import javax.net.ssl.TrustManager
|
||||
|
||||
@Singleton
|
||||
class MQTTRepository @Inject constructor(
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
) : Logging {
|
||||
class MQTTRepository @Inject constructor(private val radioConfigRepository: RadioConfigRepository) : Logging {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Quality of Service (QoS) levels in MQTT:
|
||||
* - 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.
|
||||
*/
|
||||
private const val DEFAULT_QOS = 1
|
||||
|
|
@ -84,63 +83,72 @@ class MQTTRepository @Inject constructor(
|
|||
|
||||
val rootTopic = mqttConfig.root.ifEmpty { DEFAULT_TOPIC_ROOT }
|
||||
|
||||
val connectOptions = MqttConnectOptions().apply {
|
||||
userName = mqttConfig.username
|
||||
password = mqttConfig.password.toCharArray()
|
||||
isAutomaticReconnect = true
|
||||
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/+")
|
||||
val connectOptions =
|
||||
MqttConnectOptions().apply {
|
||||
userName = mqttConfig.username
|
||||
password = mqttConfig.password.toCharArray()
|
||||
isAutomaticReconnect = true
|
||||
if (mqttConfig.tlsEnabled) {
|
||||
socketFactory = sslContext.socketFactory
|
||||
}
|
||||
subscribe("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+")
|
||||
}
|
||||
|
||||
override fun connectionLost(cause: Throwable) {
|
||||
info("MQTT connectionLost cause: $cause")
|
||||
if (cause is IllegalArgumentException) close(cause)
|
||||
val bufferOptions =
|
||||
DisconnectedBufferOptions().apply {
|
||||
isBufferEnabled = true
|
||||
bufferSize = 512
|
||||
isPersistBuffer = false
|
||||
isDeleteOldestMessages = true
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String, message: MqttMessage) {
|
||||
trySend(mqttClientProxyMessage {
|
||||
this.topic = topic
|
||||
data = ByteString.copyFrom(message.payload)
|
||||
retained = message.isRetained
|
||||
})
|
||||
}
|
||||
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 deliveryComplete(token: IMqttDeliveryToken?) {
|
||||
info("MQTT deliveryComplete messageId: ${token?.messageId}")
|
||||
override fun connectionLost(cause: Throwable) {
|
||||
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 (host, port) = mqttConfig.address.ifEmpty { DEFAULT_SERVER_ADDRESS }
|
||||
.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) }
|
||||
val (host, port) =
|
||||
mqttConfig.address
|
||||
.ifEmpty { DEFAULT_SERVER_ADDRESS }
|
||||
.split(":", limit = 2)
|
||||
.let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) }
|
||||
|
||||
mqttClient = MqttAsyncClient(
|
||||
URI(scheme, null, host, port, "", "", "").toString(),
|
||||
ownerId,
|
||||
MemoryPersistence(),
|
||||
).apply {
|
||||
setCallback(callback)
|
||||
setBufferOpts(bufferOptions)
|
||||
connect(connectOptions)
|
||||
}
|
||||
mqttClient =
|
||||
MqttAsyncClient(URI(scheme, null, host, port, "", "", "").toString(), ownerId, MemoryPersistence()).apply {
|
||||
setCallback(callback)
|
||||
setBufferOpts(bufferOptions)
|
||||
connect(connectOptions)
|
||||
}
|
||||
|
||||
awaitClose { disconnect() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ dependencies {
|
|||
kover(projects.app)
|
||||
kover(projects.meshServiceExample)
|
||||
|
||||
kover(projects.core.data)
|
||||
kover(projects.core.datastore)
|
||||
kover(projects.core.model)
|
||||
kover(projects.core.navigation)
|
||||
kover(projects.core.network)
|
||||
|
|
|
|||
25
core/data/build.gradle.kts
Normal file
25
core/data/build.gradle.kts
Normal 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
core/datastore/build.gradle.kts
Normal file
33
core/datastore/build.gradle.kts
Normal 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/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
package org.meshtastic.core.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.ChannelProtos.Channel
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Class that handles saving and retrieving [ChannelSet] data.
|
||||
*/
|
||||
class ChannelSetRepository @Inject constructor(
|
||||
private val channelSetStore: DataStore<ChannelSet>
|
||||
) : Logging {
|
||||
val channelSetFlow: Flow<ChannelSet> = channelSetStore.data
|
||||
.catch { exception ->
|
||||
/** Class that handles saving and retrieving [ChannelSet] data. */
|
||||
@Singleton
|
||||
class ChannelSetDataSource @Inject constructor(private val channelSetStore: DataStore<ChannelSet>) {
|
||||
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}")
|
||||
Timber.e("Error reading DeviceConfig settings: ${exception.message}")
|
||||
emit(ChannelSet.getDefaultInstance())
|
||||
} else {
|
||||
throw exception
|
||||
|
|
@ -46,26 +44,18 @@ class ChannelSetRepository @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun clearChannelSet() {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().clear().build()
|
||||
}
|
||||
channelSetStore.updateData { preference -> preference.toBuilder().clear().build() }
|
||||
}
|
||||
|
||||
suspend fun clearSettings() {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().clearSettings().build()
|
||||
}
|
||||
channelSetStore.updateData { preference -> preference.toBuilder().clearSettings().build() }
|
||||
}
|
||||
|
||||
suspend fun addAllSettings(settingsList: List<ChannelSettings>) {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().addAllSettings(settingsList).build()
|
||||
}
|
||||
channelSetStore.updateData { preference -> 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) {
|
||||
if (channel.role == Channel.Role.DISABLED) return
|
||||
channelSetStore.updateData { preference ->
|
||||
|
|
@ -80,8 +70,6 @@ class ChannelSetRepository @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().setLoraConfig(config).build()
|
||||
}
|
||||
channelSetStore.updateData { preference -> preference.toBuilder().setLoraConfig(config).build() }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,28 +15,26 @@
|
|||
* 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 com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Class that handles saving and retrieving [LocalConfig] data.
|
||||
*/
|
||||
class LocalConfigRepository @Inject constructor(
|
||||
private val localConfigStore: DataStore<LocalConfig>,
|
||||
) : Logging {
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigStore.data
|
||||
.catch { exception ->
|
||||
/** Class that handles saving and retrieving [LocalConfig] data. */
|
||||
@Singleton
|
||||
class LocalConfigDataSource @Inject constructor(private val localConfigStore: DataStore<LocalConfig>) {
|
||||
val localConfigFlow: Flow<LocalConfig> =
|
||||
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}")
|
||||
Timber.e("Error reading LocalConfig settings: ${exception.message}")
|
||||
emit(LocalConfig.getDefaultInstance())
|
||||
} else {
|
||||
throw exception
|
||||
|
|
@ -44,14 +42,10 @@ class LocalConfigRepository @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun clearLocalConfig() {
|
||||
localConfigStore.updateData { preference ->
|
||||
preference.toBuilder().clear().build()
|
||||
}
|
||||
localConfigStore.updateData { preference -> preference.toBuilder().clear().build() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates [LocalConfig] from each [Config] oneOf.
|
||||
*/
|
||||
/** Updates [LocalConfig] from each [Config] oneOf. */
|
||||
suspend fun setLocalConfig(config: Config) = localConfigStore.updateData {
|
||||
val builder = it.toBuilder()
|
||||
config.allFields.forEach { (field, value) ->
|
||||
|
|
@ -59,7 +53,7 @@ class LocalConfigRepository @Inject constructor(
|
|||
if (localField != null) {
|
||||
builder.setField(localField, value)
|
||||
} else {
|
||||
errormsg("Error writing LocalConfig settings: ${config.payloadVariantCase}")
|
||||
Timber.e("Error writing LocalConfig settings: ${config.payloadVariantCase}")
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
|
|
@ -15,28 +15,26 @@
|
|||
* 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 com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Class that handles saving and retrieving [LocalModuleConfig] data.
|
||||
*/
|
||||
class ModuleConfigRepository @Inject constructor(
|
||||
private val moduleConfigStore: DataStore<LocalModuleConfig>,
|
||||
) : Logging {
|
||||
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigStore.data
|
||||
.catch { exception ->
|
||||
/** Class that handles saving and retrieving [LocalModuleConfig] data. */
|
||||
@Singleton
|
||||
class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore: DataStore<LocalModuleConfig>) {
|
||||
val moduleConfigFlow: Flow<LocalModuleConfig> =
|
||||
moduleConfigStore.data.catch { exception ->
|
||||
// dataStore.data throws an IOException when an error is encountered when reading data
|
||||
if (exception is IOException) {
|
||||
errormsg("Error reading LocalModuleConfig settings: ${exception.message}")
|
||||
Timber.e("Error reading LocalModuleConfig settings: ${exception.message}")
|
||||
emit(LocalModuleConfig.getDefaultInstance())
|
||||
} else {
|
||||
throw exception
|
||||
|
|
@ -44,14 +42,10 @@ class ModuleConfigRepository @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun clearLocalModuleConfig() {
|
||||
moduleConfigStore.updateData { preference ->
|
||||
preference.toBuilder().clear().build()
|
||||
}
|
||||
moduleConfigStore.updateData { preference -> preference.toBuilder().clear().build() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates [LocalModuleConfig] from each [ModuleConfig] oneOf.
|
||||
*/
|
||||
/** Updates [LocalModuleConfig] from each [ModuleConfig] oneOf. */
|
||||
suspend fun setLocalModuleConfig(config: ModuleConfig) = moduleConfigStore.updateData {
|
||||
val builder = it.toBuilder()
|
||||
config.allFields.forEach { (field, value) ->
|
||||
|
|
@ -59,7 +53,7 @@ class ModuleConfigRepository @Inject constructor(
|
|||
if (localField != null) {
|
||||
builder.setField(localField, value)
|
||||
} else {
|
||||
errormsg("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}")
|
||||
Timber.e("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}")
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
|
|
@ -15,15 +15,12 @@
|
|||
* 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.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
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.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -31,17 +28,13 @@ import kotlinx.serialization.SerializationException
|
|||
import kotlinx.serialization.json.Json
|
||||
import org.json.JSONArray
|
||||
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.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecentAddressesRepository
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : Logging {
|
||||
class RecentAddressesDataSource @Inject constructor(private val dataStore: DataStore<Preferences>) {
|
||||
private object PreferencesKeys {
|
||||
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
|
||||
}
|
||||
|
|
@ -53,11 +46,11 @@ constructor(
|
|||
try {
|
||||
Json.decodeFromString<List<RecentAddress>>(jsonString)
|
||||
} 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
|
||||
parseLegacyRecentAddresses(jsonString)
|
||||
} 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
|
||||
parseLegacyRecentAddresses(jsonString)
|
||||
}
|
||||
|
|
@ -76,11 +69,11 @@ constructor(
|
|||
}
|
||||
is String -> {
|
||||
// Old format: just the address string
|
||||
RecentAddress(address = item, name = context.getString(R.string.meshtastic))
|
||||
RecentAddress(address = item, name = "Meshtastic")
|
||||
}
|
||||
else -> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* 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 androidx.datastore.core.DataStore
|
||||
|
|
@ -30,16 +30,18 @@ import androidx.datastore.preferences.preferencesDataStoreFile
|
|||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddressesRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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"
|
||||
|
||||
|
|
@ -48,12 +50,9 @@ private const val USER_PREFERENCES_NAME = "user_preferences"
|
|||
object DataStoreModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext appContext: Context
|
||||
): DataStore<Preferences> =
|
||||
fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create(
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
|
||||
migrations = listOf(SharedPreferencesMigration(appContext, USER_PREFERENCES_NAME)),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
|
||||
|
|
@ -61,36 +60,22 @@ object DataStoreModule {
|
|||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideRecentAddressesRepository(
|
||||
@ApplicationContext context: Context,
|
||||
dataStore: DataStore<Preferences>,
|
||||
): RecentAddressesRepository = RecentAddressesRepository(context, dataStore)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLocalConfigDataStore(
|
||||
@ApplicationContext appContext: Context
|
||||
): DataStore<LocalConfig> =
|
||||
fun provideLocalConfigDataStore(@ApplicationContext appContext: Context): DataStore<LocalConfig> =
|
||||
DataStoreFactory.create(
|
||||
serializer = LocalConfigSerializer,
|
||||
produceFile = { appContext.dataStoreFile("local_config.pb") },
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(produceNewData = { LocalConfig.getDefaultInstance() }),
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig.getDefaultInstance() }),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideModuleConfigDataStore(
|
||||
@ApplicationContext appContext: Context
|
||||
): DataStore<LocalModuleConfig> =
|
||||
fun provideModuleConfigDataStore(@ApplicationContext appContext: Context): DataStore<LocalModuleConfig> =
|
||||
DataStoreFactory.create(
|
||||
serializer = ModuleConfigSerializer,
|
||||
produceFile = { appContext.dataStoreFile("module_config.pb") },
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(
|
||||
produceNewData = { LocalModuleConfig.getDefaultInstance() }
|
||||
),
|
||||
ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig.getDefaultInstance() }),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
|
||||
|
|
@ -100,8 +85,7 @@ object DataStoreModule {
|
|||
DataStoreFactory.create(
|
||||
serializer = ChannelSetSerializer,
|
||||
produceFile = { appContext.dataStoreFile("channel_set.pb") },
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(produceNewData = { ChannelSet.getDefaultInstance() }),
|
||||
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet.getDefaultInstance() }),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* 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
|
||||
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* 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.Serializer
|
||||
|
|
@ -24,9 +24,7 @@ import com.google.protobuf.InvalidProtocolBufferException
|
|||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for the [ChannelSet] object defined in apponly.proto.
|
||||
*/
|
||||
/** Serializer for the [ChannelSet] object defined in apponly.proto. */
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object ChannelSetSerializer : Serializer<ChannelSet> {
|
||||
override val defaultValue: ChannelSet = ChannelSet.getDefaultInstance()
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* 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.Serializer
|
||||
|
|
@ -24,9 +24,7 @@ import com.google.protobuf.InvalidProtocolBufferException
|
|||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for the [LocalConfig] object defined in localonly.proto.
|
||||
*/
|
||||
/** Serializer for the [LocalConfig] object defined in localonly.proto. */
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object LocalConfigSerializer : Serializer<LocalConfig> {
|
||||
override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance()
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* 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.Serializer
|
||||
|
|
@ -24,9 +24,7 @@ import com.google.protobuf.InvalidProtocolBufferException
|
|||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for the [LocalModuleConfig] object defined in localonly.proto.
|
||||
*/
|
||||
/** Serializer for the [LocalModuleConfig] object defined in localonly.proto. */
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object ModuleConfigSerializer : Serializer<LocalModuleConfig> {
|
||||
override val defaultValue: LocalModuleConfig = LocalModuleConfig.getDefaultInstance()
|
||||
|
|
@ -17,7 +17,7 @@ import org.gradle.kotlin.dsl.maven
|
|||
* 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")
|
||||
rootProject.name = "MeshtasticAndroid"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue