From 53fdda3a9cc6320e9f6c903983a270ac678080c6 Mon Sep 17 00:00:00 2001 From: Phil Oliver <3497406+poliver@users.noreply.github.com> Date: Mon, 22 Sep 2025 23:49:28 -0400 Subject: [PATCH] Add core data modules (#3169) --- app/build.gradle.kts | 2 + .../com/geeksville/mesh/model/BTScanModel.kt | 14 +-- .../datastore/RadioConfigRepository.kt | 33 +++--- .../mesh/repository/network/MQTTRepository.kt | 110 ++++++++++-------- build.gradle.kts | 2 + core/data/build.gradle.kts | 25 ++++ core/datastore/build.gradle.kts | 33 ++++++ .../core/datastore/ChannelSetDataSource.kt | 40 +++---- .../core/datastore/LocalConfigDataSource.kt | 30 ++--- .../core/datastore/ModuleConfigDataSource.kt | 32 +++-- .../datastore/RecentAddressesDataSource.kt | 23 ++-- .../core/datastore/di}/DataStoreModule.kt | 40 ++----- .../core/datastore/model}/RecentAddress.kt | 2 +- .../serializer}/ChannelSetSerializer.kt | 6 +- .../serializer}/LocalConfigSerializer.kt | 6 +- .../serializer}/ModuleConfigSerializer.kt | 6 +- settings.gradle.kts | 2 +- 17 files changed, 213 insertions(+), 193 deletions(-) create mode 100644 core/data/build.gradle.kts create mode 100644 core/datastore/build.gradle.kts rename app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt (68%) rename app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt (71%) rename app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt (71%) rename app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddressesRepository.kt => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt (82%) rename {app/src/main/java/com/geeksville/mesh/repository/datastore => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di}/DataStoreModule.kt (71%) rename {app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model}/RecentAddress.kt (92%) rename {app/src/main/java/com/geeksville/mesh/repository/datastore => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer}/ChannelSetSerializer.kt (92%) rename {app/src/main/java/com/geeksville/mesh/repository/datastore => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer}/LocalConfigSerializer.kt (92%) rename {app/src/main/java/com/geeksville/mesh/repository/datastore => core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer}/ModuleConfigSerializer.kt (92%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 22758fe5d..4220da66a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index 271c2689d..10e2337de 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -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, 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> = - 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> = - 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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt index 06e27095f..6bb7fa328 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/datastore/RadioConfigRepository.kt @@ -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 = channelSetRepository.channelSetFlow + val channelSetFlow: Flow = 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) { - 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 = localConfigRepository.localConfigFlow + val localConfigFlow: Flow = 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 = moduleConfigRepository.moduleConfigFlow + val moduleConfigFlow: Flow = 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. */ diff --git a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt index dada5b221..56a6a8c8e 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/network/MQTTRepository.kt @@ -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() } } diff --git a/build.gradle.kts b/build.gradle.kts index c27963470..988de5ba6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 000000000..313d80686 --- /dev/null +++ b/core/data/build.gradle.kts @@ -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 . + */ + +plugins { + alias(libs.plugins.meshtastic.android.library) + alias(libs.plugins.kover) +} + +android { namespace = "org.meshtastic.core.data" } + +dependencies {} diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 000000000..edd017c00 --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -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 . + */ + +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) +} diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt similarity index 68% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt index dad611bd1..7fdfc274d 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetRepository.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt @@ -15,30 +15,28 @@ * along with this program. If not, see . */ -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 -) : Logging { - val channelSetFlow: Flow = channelSetStore.data - .catch { exception -> +/** Class that handles saving and retrieving [ChannelSet] data. */ +@Singleton +class ChannelSetDataSource @Inject constructor(private val channelSetStore: DataStore) { + val channelSetFlow: Flow = + channelSetStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { - errormsg("Error reading DeviceConfig settings: ${exception.message}") + 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) { - 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() } } } diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt similarity index 71% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt index 9f8255d8c..77f96605e 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigRepository.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt @@ -15,28 +15,26 @@ * along with this program. If not, see . */ -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, -) : Logging { - val localConfigFlow: Flow = localConfigStore.data - .catch { exception -> +/** Class that handles saving and retrieving [LocalConfig] data. */ +@Singleton +class LocalConfigDataSource @Inject constructor(private val localConfigStore: DataStore) { + val localConfigFlow: Flow = + localConfigStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { - errormsg("Error reading LocalConfig settings: ${exception.message}") + 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() diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt similarity index 71% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt index 2a4316594..be8900fd0 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigRepository.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt @@ -15,28 +15,26 @@ * along with this program. If not, see . */ -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, -) : Logging { - val moduleConfigFlow: Flow = moduleConfigStore.data - .catch { exception -> +/** Class that handles saving and retrieving [LocalModuleConfig] data. */ +@Singleton +class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore: DataStore) { + val moduleConfigFlow: Flow = + 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() diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddressesRepository.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt similarity index 82% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddressesRepository.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt index 10248ad0b..a8d6d0db1 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddressesRepository.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt @@ -15,15 +15,12 @@ * along with this program. If not, see . */ -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, -) : Logging { +class RecentAddressesDataSource @Inject constructor(private val dataStore: DataStore) { private object PreferencesKeys { val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses") } @@ -53,11 +46,11 @@ constructor( try { Json.decodeFromString>(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 } } diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di/DataStoreModule.kt similarity index 71% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di/DataStoreModule.kt index 0ab3f112e..b92978590 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/DataStoreModule.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di/DataStoreModule.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -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 = + fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore = 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, - ): RecentAddressesRepository = RecentAddressesRepository(context, dataStore) - - @Singleton - @Provides - fun provideLocalConfigDataStore( - @ApplicationContext appContext: Context - ): DataStore = + fun provideLocalConfigDataStore(@ApplicationContext appContext: Context): DataStore = 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 = + fun provideModuleConfigDataStore(@ApplicationContext appContext: Context): DataStore = 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()), ) } diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddress.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt similarity index 92% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddress.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt index e5c6039e4..4cbb90320 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/recentaddresses/RecentAddress.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.geeksville.mesh.repository.datastore.recentaddresses +package org.meshtastic.core.datastore.model import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt similarity index 92% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt index 73a0b225e..1a1d33605 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/ChannelSetSerializer.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -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 { override val defaultValue: ChannelSet = ChannelSet.getDefaultInstance() diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt similarity index 92% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt index 5e04c9355..3abea260f 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/LocalConfigSerializer.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -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 { override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance() diff --git a/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigSerializer.kt b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt similarity index 92% rename from app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigSerializer.kt rename to core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt index 8eb185f9d..b774534f4 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/datastore/ModuleConfigSerializer.kt +++ b/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -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 { override val defaultValue: LocalModuleConfig = LocalModuleConfig.getDefaultInstance() diff --git a/settings.gradle.kts b/settings.gradle.kts index 251fbc7ac..285e6551d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,7 +17,7 @@ import org.gradle.kotlin.dsl.maven * along with this program. If not, see . */ -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"