mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: settings rework part 2, domain and usecase abstraction, tests (#4680)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
5f31df96d8
commit
8c6bd8ab7a
121 changed files with 5245 additions and 1332 deletions
|
|
@ -198,7 +198,7 @@
|
|||
<intent-filter android:autoVerify="true">
|
||||
<!--
|
||||
The QR codes to share channel settings and contacts are shared as meshtastic URLS.
|
||||
We also support NFC NDEF Discovery for the same URLs.
|
||||
We also support NFC NDEF Discovery for the same URLS.
|
||||
|
||||
An approximate example:
|
||||
https://meshtastic.org/e/YXNkZnF3ZXJhc2RmcXdlcmFzZGZxd2Vy
|
||||
|
|
@ -252,9 +252,9 @@
|
|||
android:path="com.geeksville.mesh" /> -->
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="com.geeksville.mesh.service.ReplyReceiver"/>
|
||||
<receiver android:name="com.geeksville.mesh.service.MarkAsReadReceiver"/>
|
||||
<receiver android:name="com.geeksville.mesh.service.ReactionReceiver"/>
|
||||
<receiver android:name="com.geeksville.mesh.service.ReplyReceiver" android:exported="false" />
|
||||
<receiver android:name="com.geeksville.mesh.service.MarkAsReadReceiver" android:exported="false" />
|
||||
<receiver android:name="com.geeksville.mesh.service.ReactionReceiver" android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name="com.geeksville.mesh.widget.LocalStatsWidgetReceiver"
|
||||
|
|
@ -277,6 +277,17 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -208,6 +208,10 @@ constructor(
|
|||
onDisconnected(state)
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
Logger.w(e) { "[$address] bleConnection.connectionState flow crashed!" }
|
||||
service.onDisconnect(BleError.from(e))
|
||||
}
|
||||
.launchIn(connectionScope)
|
||||
|
||||
val p = retryBleOperation(tag = address) { findPeripheral() }
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -47,9 +48,9 @@ import org.meshtastic.core.common.util.nowMillis
|
|||
import org.meshtastic.core.common.util.toRemoteExceptions
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.di.ProcessLifecycle
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import javax.inject.Inject
|
||||
|
|
@ -127,6 +128,7 @@ constructor(
|
|||
stopInterface()
|
||||
}
|
||||
}
|
||||
.catch { Logger.e(it) { "bluetoothRepository.state flow crashed!" } }
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
|
||||
networkRepository.networkAvailable
|
||||
|
|
@ -137,6 +139,7 @@ constructor(
|
|||
stopInterface()
|
||||
}
|
||||
}
|
||||
.catch { Logger.e(it) { "networkRepository.networkAvailable flow crashed!" } }
|
||||
.launchIn(processLifecycle.coroutineScope)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ package com.geeksville.mesh.service
|
|||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ import okio.ByteString.Companion.toByteString
|
|||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.isWithinSizeLimit
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Constants
|
||||
|
|
@ -47,7 +47,6 @@ import org.meshtastic.proto.NeighborInfo
|
|||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
|
|
@ -68,7 +67,6 @@ constructor(
|
|||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val currentPacketId = AtomicLong(java.util.Random(nowMillis).nextLong().absoluteValue)
|
||||
private val sessionPasskey = AtomicReference(ByteString.EMPTY)
|
||||
private val offlineSentPackets = CopyOnWriteArrayList<DataPacket>()
|
||||
val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
|
||||
val neighborInfoStartTimes = ConcurrentHashMap<Int, Long>()
|
||||
|
||||
|
|
@ -77,17 +75,6 @@ constructor(
|
|||
|
||||
@Volatile var lastNeighborInfo: NeighborInfo? = null
|
||||
|
||||
private val rememberDataType =
|
||||
setOf(
|
||||
PortNum.TEXT_MESSAGE_APP.value,
|
||||
PortNum.ALERT_APP.value,
|
||||
PortNum.WAYPOINT_APP.value,
|
||||
PortNum.ATAK_PLUGIN.value,
|
||||
PortNum.ATAK_FORWARDER.value,
|
||||
PortNum.DETECTION_SENSOR_APP.value,
|
||||
PortNum.PRIVATE_APP.value,
|
||||
)
|
||||
|
||||
fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
radioConfigRepository?.localConfigFlow?.onEach { localConfig.value = it }?.launchIn(scope)
|
||||
|
|
@ -154,14 +141,9 @@ constructor(
|
|||
}
|
||||
|
||||
if (connectionStateHolder?.connectionState?.value == ConnectionState.Connected) {
|
||||
try {
|
||||
sendNow(p)
|
||||
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
|
||||
Logger.e(ex) { "Error sending message, so enqueueing" }
|
||||
enqueueForSending(p)
|
||||
}
|
||||
sendNow(p)
|
||||
} else {
|
||||
enqueueForSending(p)
|
||||
error("Radio is not connected")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,25 +167,6 @@ constructor(
|
|||
packetHandler?.sendToRadio(meshPacket)
|
||||
}
|
||||
|
||||
private fun enqueueForSending(p: DataPacket) {
|
||||
if (p.dataType in rememberDataType) {
|
||||
offlineSentPackets.add(p)
|
||||
}
|
||||
}
|
||||
|
||||
fun processQueuedPackets() {
|
||||
val sentPackets = mutableListOf<DataPacket>()
|
||||
offlineSentPackets.forEach { p ->
|
||||
try {
|
||||
sendNow(p)
|
||||
sentPackets.add(p)
|
||||
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
|
||||
Logger.e(ex) { "Error sending queued message:" }
|
||||
}
|
||||
}
|
||||
offlineSentPackets.removeAll(sentPackets)
|
||||
}
|
||||
|
||||
fun sendAdmin(
|
||||
destNum: Int,
|
||||
requestId: Int = generatePacketId(),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import org.meshtastic.core.data.repository.NodeRepository
|
|||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ package com.geeksville.mesh.service
|
|||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import com.geeksville.mesh.widget.LocalStatsWidget
|
||||
|
|
@ -40,7 +44,9 @@ import org.meshtastic.core.common.util.handledLaunch
|
|||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.nowSeconds
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -50,8 +56,8 @@ import org.meshtastic.core.resources.device_sleeping
|
|||
import org.meshtastic.core.resources.disconnected
|
||||
import org.meshtastic.core.resources.getString
|
||||
import org.meshtastic.core.resources.meshtastic_app_name
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
|
@ -82,6 +88,8 @@ constructor(
|
|||
private val commandSender: MeshCommandSender,
|
||||
private val nodeManager: MeshNodeManager,
|
||||
private val analytics: PlatformAnalytics,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val workManager: WorkManager,
|
||||
) {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var sleepTimeout: Job? = null
|
||||
|
|
@ -255,7 +263,25 @@ constructor(
|
|||
}
|
||||
|
||||
fun onRadioConfigLoaded() {
|
||||
commandSender.processQueuedPackets()
|
||||
scope.handledLaunch {
|
||||
val queuedPackets = packetRepository.getQueuedPackets() ?: emptyList()
|
||||
queuedPackets.forEach { packet ->
|
||||
try {
|
||||
val workRequest =
|
||||
OneTimeWorkRequestBuilder<SendMessageWorker>()
|
||||
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packet.id))
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
"${SendMessageWorker.WORK_NAME_PREFIX}${packet.id}",
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
workRequest,
|
||||
)
|
||||
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
|
||||
Logger.e(e) { "Failed to enqueue queued packet worker" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val myNodeNum = nodeManager.myNodeNum ?: 0
|
||||
// Set time
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ constructor(
|
|||
private val logInsertJobByPacketId = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
private val earlyReceivedPackets = ArrayDeque<MeshPacket>()
|
||||
private val maxEarlyPacketBuffer = 128
|
||||
private val maxEarlyPacketBuffer = 10240
|
||||
|
||||
fun clearEarlyPackets() {
|
||||
synchronized(earlyReceivedPackets) { earlyReceivedPackets.clear() }
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ constructor(
|
|||
|
||||
fun loadCachedNodeDB() {
|
||||
scope.handledLaunch {
|
||||
val nodes = nodeRepository?.getNodeDBbyNum()?.first() ?: emptyMap()
|
||||
val nodes = nodeRepository?.getNodeEntityDBbyNumFlow()?.first() ?: emptyMap()
|
||||
nodeDBbyNodeNum.putAll(nodes)
|
||||
nodes.values.forEach { nodeDBbyID[it.user.id] = it }
|
||||
myNodeNum = nodeRepository?.myNodeInfo?.value?.myNodeNum
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import android.content.Intent
|
|||
import android.os.Parcelable
|
||||
import co.touchlab.kermit.Logger
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ constructor(
|
|||
if (myNodeNum != null) {
|
||||
// We use runBlocking here because this is called from MeshConnectionManager's synchronous methods,
|
||||
// and we only do this once if the cache is empty.
|
||||
val nodes = runBlocking { repo.getNodeDBbyNum().first() }
|
||||
val nodes = runBlocking { repo.getNodeEntityDBbyNumFlow().first() }
|
||||
nodes[myNodeNum]?.let { entity ->
|
||||
if (cachedDeviceMetrics == null) {
|
||||
cachedDeviceMetrics = entity.deviceTelemetry.device_metrics
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ import org.meshtastic.core.common.util.nowMillis
|
|||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.QueueStatus
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ import no.nordicsemi.android.common.permissions.notification.RequestNotification
|
|||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.navigation.ConnectionsRoutes
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
|
|
@ -123,7 +124,6 @@ import org.meshtastic.core.resources.should_update
|
|||
import org.meshtastic.core.resources.should_update_firmware
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.resources.view_on_map
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.icon.Conversations
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import com.geeksville.mesh.ui.connections.components.UsbDevices
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -71,7 +72,6 @@ import org.meshtastic.core.resources.must_set_region
|
|||
import org.meshtastic.core.resources.no_device_selected
|
||||
import org.meshtastic.core.resources.not_connected
|
||||
import org.meshtastic.core.resources.set_your_region
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ import no.nordicsemi.android.common.scanner.view.ScannerView
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bluetooth_available_devices
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
|
||||
/**
|
||||
* Composable that displays a list of Bluetooth Low Energy (BLE) devices and allows scanning. It handles Bluetooth
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.connections.components
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
|
|
@ -39,7 +38,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.connections.DeviceType
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.ui.icon.Device
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.NoDevice
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ import com.geeksville.mesh.model.DeviceListEntry
|
|||
import kotlinx.coroutines.delay
|
||||
import no.nordicsemi.android.common.ui.view.RssiIcon
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add
|
||||
import org.meshtastic.core.resources.bluetooth
|
||||
import org.meshtastic.core.resources.network
|
||||
import org.meshtastic.core.resources.serial
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.NodeChip
|
||||
|
||||
private const val RSSI_UPDATE_RATE_MS = 2000L
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
|
||||
@Composable
|
||||
fun List<DeviceListEntry>.DeviceListSection(
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import com.geeksville.mesh.ui.connections.ScannerViewModel
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.isValidAddress
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_network_device
|
||||
import org.meshtastic.core.resources.address
|
||||
|
|
@ -63,7 +64,6 @@ import org.meshtastic.core.resources.forget_connection
|
|||
import org.meshtastic.core.resources.ip_port
|
||||
import org.meshtastic.core.resources.no_network_devices
|
||||
import org.meshtastic.core.resources.recent_network_devices
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.MeshtasticResourceDialog
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
import com.geeksville.mesh.ui.connections.ScannerViewModel
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.no_usb_devices
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
|
||||
@Composable
|
||||
fun UsbDevices(
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ import kotlinx.coroutines.launch
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.toPlatformUri
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.util.getChannelUrl
|
||||
import org.meshtastic.core.model.util.qrCode
|
||||
import org.meshtastic.core.navigation.Route
|
||||
|
|
@ -88,7 +89,6 @@ import org.meshtastic.core.resources.replace
|
|||
import org.meshtastic.core.resources.reset
|
||||
import org.meshtastic.core.resources.reset_to_defaults
|
||||
import org.meshtastic.core.resources.share_channels_qr
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.AdaptiveTwoPane
|
||||
import org.meshtastic.core.ui.component.ChannelSelection
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ import dagger.hilt.android.EntryPointAccessors
|
|||
import dagger.hilt.components.SingletonComponent
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.DateFormatter
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.air_utilization
|
||||
|
|
@ -92,7 +93,6 @@ import org.meshtastic.core.resources.powered
|
|||
import org.meshtastic.core.resources.refresh
|
||||
import org.meshtastic.core.resources.updated
|
||||
import org.meshtastic.core.resources.uptime
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
|
||||
class LocalStatsWidget : GlanceAppWidget() {
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ import org.junit.Assert.assertTrue
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okio.ByteString
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
||||
class MeshCommandSenderQueueTest {
|
||||
|
||||
private val packetHandler = mockk<PacketHandler>(relaxed = true)
|
||||
private val connectionStateHandler = mockk<ConnectionStateHandler>(relaxed = true)
|
||||
private val connectionStateFlow = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
|
||||
private lateinit var commandSender: MeshCommandSender
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { connectionStateHandler.connectionState } returns connectionStateFlow.asStateFlow()
|
||||
commandSender = MeshCommandSender(packetHandler, null, connectionStateHandler, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues TEXT_MESSAGE_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.TEXT_MESSAGE_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues ATAK_PLUGIN when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.ATAK_PLUGIN.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues ATAK_FORWARDER when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.ATAK_FORWARDER.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues DETECTION_SENSOR_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.DETECTION_SENSOR_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData queues PRIVATE_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.PRIVATE_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 1) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sendData does NOT queue IP_TUNNEL_APP when disconnected`() {
|
||||
val packet = DataPacket(dataType = PortNum.IP_TUNNEL_APP.value, bytes = ByteString.EMPTY)
|
||||
commandSender.sendData(packet)
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
commandSender.processQueuedPackets()
|
||||
|
||||
verify(exactly = 0) { packetHandler.sendToRadio(any<org.meshtastic.proto.MeshPacket>()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@ package com.geeksville.mesh.service
|
|||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
|
|
@ -37,12 +40,15 @@ import org.junit.Before
|
|||
import org.junit.Test
|
||||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
|
|
@ -67,6 +73,8 @@ class MeshConnectionManagerTest {
|
|||
private val commandSender: MeshCommandSender = mockk(relaxed = true)
|
||||
private val nodeManager: MeshNodeManager = mockk(relaxed = true)
|
||||
private val analytics: PlatformAnalytics = mockk(relaxed = true)
|
||||
private val packetRepository: PacketRepository = mockk(relaxed = true)
|
||||
private val workManager: WorkManager = mockk(relaxed = true)
|
||||
private val radioConnectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
private val localConfigFlow = MutableStateFlow(LocalConfig())
|
||||
private val moduleConfigFlow = MutableStateFlow(LocalModuleConfig())
|
||||
|
|
@ -107,6 +115,8 @@ class MeshConnectionManagerTest {
|
|||
commandSender,
|
||||
nodeManager,
|
||||
analytics,
|
||||
packetRepository,
|
||||
workManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -194,10 +204,23 @@ class MeshConnectionManagerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `onRadioConfigLoaded processes queued packets and sets time`() = runTest(testDispatcher) {
|
||||
manager.onRadioConfigLoaded()
|
||||
fun `onRadioConfigLoaded enqueues queued packets and sets time`() = runTest(testDispatcher) {
|
||||
manager.start(backgroundScope)
|
||||
val packetId = 456
|
||||
val dataPacket = mockk<DataPacket>(relaxed = true)
|
||||
every { dataPacket.id } returns packetId
|
||||
coEvery { packetRepository.getQueuedPackets() } returns listOf(dataPacket)
|
||||
|
||||
verify { commandSender.processQueuedPackets() }
|
||||
manager.onRadioConfigLoaded()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify {
|
||||
workManager.enqueueUniqueWork(
|
||||
match<String> { it.startsWith(SendMessageWorker.WORK_NAME_PREFIX) },
|
||||
any<ExistingWorkPolicy>(),
|
||||
any<OneTimeWorkRequest>(),
|
||||
)
|
||||
}
|
||||
verify { commandSender.sendAdmin(any(), initFn = any()) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import org.junit.Test
|
|||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging
|
||||
package org.meshtastic.core.common.util
|
||||
|
||||
/**
|
||||
* This util class allows you to optimize the binary size of the transmitted text message strings. It replaces certain
|
||||
|
|
@ -24,7 +24,7 @@ package org.meshtastic.feature.messaging
|
|||
* reduces the binary size of the transmitted message. The average transmitted message volume can then fit around
|
||||
* ~140-145 characters instead of ~115-120
|
||||
*/
|
||||
internal object HomoglyphCharacterStringTransformer {
|
||||
object HomoglyphCharacterStringTransformer {
|
||||
|
||||
/**
|
||||
* Unicode characters from the basic cyrillic block (U+0400-U+04FF), each of which occupies 2 bytes
|
||||
|
|
@ -25,8 +25,9 @@ import java.util.concurrent.atomic.AtomicReference
|
|||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* A helper class that manages a single [Job]. When a new job is launched, the previous one is cancelled. This is useful
|
||||
* for ensuring that only one operation of a certain type is running at a time.
|
||||
* A helper class that manages a single [Job]. When a new job is launched, any previous job is cancelled. This is useful
|
||||
* for ensuring that only the latest operation of a certain type is running at a time (e.g. for search or settings
|
||||
* updates).
|
||||
*/
|
||||
class SequentialJob @Inject constructor() {
|
||||
private val job = AtomicReference<Job?>()
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ import javax.inject.Singleton
|
|||
/** Repository for managing node-related data, including hardware info, node database, and identity. */
|
||||
@Singleton
|
||||
@Suppress("TooManyFunctions")
|
||||
class NodeRepository
|
||||
open class NodeRepository
|
||||
@Inject
|
||||
constructor(
|
||||
@ProcessLifecycle private val processLifecycle: Lifecycle,
|
||||
|
|
@ -66,7 +66,7 @@ constructor(
|
|||
private val localStatsDataSource: LocalStatsDataSource,
|
||||
) {
|
||||
/** Hardware info about our local device (can be null if not connected). */
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?> =
|
||||
open val myNodeInfo: StateFlow<MyNodeEntity?> =
|
||||
nodeInfoReadDataSource
|
||||
.myNodeInfoFlow()
|
||||
.flowOn(dispatchers.io)
|
||||
|
|
@ -75,7 +75,7 @@ constructor(
|
|||
private val _ourNodeInfo = MutableStateFlow<Node?>(null)
|
||||
|
||||
/** Information about the locally connected node, as seen from the mesh. */
|
||||
val ourNodeInfo: StateFlow<Node?>
|
||||
open val ourNodeInfo: StateFlow<Node?>
|
||||
get() = _ourNodeInfo
|
||||
|
||||
private val _myId = MutableStateFlow<String?>(null)
|
||||
|
|
@ -131,7 +131,7 @@ constructor(
|
|||
.map { info -> if (nodeNum == info?.myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum }
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun getNodeDBbyNum() =
|
||||
fun getNodeEntityDBbyNumFlow() =
|
||||
nodeInfoReadDataSource.nodeDBbyNumFlow().map { map -> map.mapValues { (_, it) -> it.toEntity() } }
|
||||
|
||||
/** Returns the [Node] associated with a given [userId]. Falls back to a generic node if not found. */
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import javax.inject.Inject
|
|||
* Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] &
|
||||
* [LocalModuleConfig].
|
||||
*/
|
||||
class RadioConfigRepository
|
||||
open class RadioConfigRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeDB: NodeRepository,
|
||||
|
|
@ -68,7 +68,7 @@ constructor(
|
|||
suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel)
|
||||
|
||||
/** Flow representing the [LocalConfig] data store. */
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigDataSource.localConfigFlow
|
||||
open val localConfigFlow: Flow<LocalConfig> = localConfigDataSource.localConfigFlow
|
||||
|
||||
/** Clears the [LocalConfig] data in the data store. */
|
||||
suspend fun clearLocalConfig() {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,12 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
@Suppress("TooManyFunctions")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DatabaseManager @Inject constructor(private val app: Application, private val dispatchers: CoroutineDispatchers) {
|
||||
open class DatabaseManager
|
||||
@Inject
|
||||
constructor(
|
||||
private val app: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
val prefs: SharedPreferences = app.getSharedPreferences("db-manager-prefs", Context.MODE_PRIVATE)
|
||||
private val managerScope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
|
|
@ -54,7 +59,7 @@ class DatabaseManager @Inject constructor(private val app: Application, private
|
|||
|
||||
// Expose the DB cache limit as a reactive stream so UI can observe changes.
|
||||
private val _cacheLimit = MutableStateFlow(getCacheLimit())
|
||||
val cacheLimit: StateFlow<Int> = _cacheLimit
|
||||
open val cacheLimit: StateFlow<Int> = _cacheLimit
|
||||
|
||||
// Keep cache-limit StateFlow in sync if some other component updates SharedPreferences.
|
||||
private val prefsListener =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -40,4 +40,4 @@ plugins {
|
|||
|
||||
configure<LibraryExtension> { namespace = "org.meshtastic.core.di" }
|
||||
|
||||
dependencies {}
|
||||
dependencies { implementation(libs.androidx.work.runtime.ktx) }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2025-2026 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
|
||||
|
|
@ -14,14 +14,17 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.WorkManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
|
@ -30,4 +33,8 @@ object AppModule {
|
|||
@Provides
|
||||
fun provideCoroutineDispatchers(): CoroutineDispatchers =
|
||||
CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Main, default = Dispatchers.Default)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWorkManager(@ApplicationContext context: Context): WorkManager = WorkManager.getInstance(context)
|
||||
}
|
||||
|
|
|
|||
44
core/domain/build.gradle.kts
Normal file
44
core/domain/build.gradle.kts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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.android.library.flavors)
|
||||
alias(libs.plugins.meshtastic.hilt)
|
||||
}
|
||||
|
||||
android { namespace = "org.meshtastic.core.domain" }
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.proto)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.datastore)
|
||||
implementation(projects.core.resources)
|
||||
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.compose.multiplatform.resources)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.turbine)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain
|
||||
|
||||
/**
|
||||
* Interface for enqueuing background work for transmitting messages. This allows the domain layer to trigger durable
|
||||
* transmission without depending on Android-specific WorkManager.
|
||||
*/
|
||||
interface MessageQueue {
|
||||
suspend fun enqueue(packetId: Int)
|
||||
}
|
||||
|
|
@ -14,28 +14,39 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.usecase
|
||||
package org.meshtastic.core.domain.usecase
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.domain.MessageQueue
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.feature.messaging.HomoglyphCharacterStringTransformer
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Use case for sending a message. This component handles message transformation, persistence, and enqueuing for durable
|
||||
* delivery.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
class SendMessageUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val radioController: RadioController,
|
||||
private val homoglyphEncodingPrefs: HomoglyphPrefs,
|
||||
private val messageQueue: MessageQueue,
|
||||
) {
|
||||
|
||||
@Suppress("NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
@ -74,18 +85,45 @@ constructor(
|
|||
text
|
||||
}
|
||||
|
||||
val packet = DataPacket(dest, channel ?: 0, finalMessageText, replyId).apply { from = fromId }
|
||||
val packetId = abs(Random.nextInt())
|
||||
|
||||
val packet =
|
||||
DataPacket(dest, channel ?: 0, finalMessageText, replyId).apply {
|
||||
from = fromId
|
||||
id = packetId
|
||||
status = MessageStatus.QUEUED
|
||||
}
|
||||
|
||||
val packetToSave =
|
||||
Packet(
|
||||
uuid = 0L,
|
||||
myNodeNum = ourNode?.num ?: 0,
|
||||
packetId = packetId,
|
||||
port_num = packet.dataType,
|
||||
contact_key = contactKey,
|
||||
received_time = nowMillis,
|
||||
read = true,
|
||||
data = packet,
|
||||
snr = packet.snr,
|
||||
rssi = packet.rssi,
|
||||
hopsAway = packet.hopsAway,
|
||||
filtered = false,
|
||||
)
|
||||
|
||||
try {
|
||||
serviceRepository.meshService?.send(packet)
|
||||
// Write to the DB to immediately reflect the queued state on the UI
|
||||
packetRepository.insert(packetToSave)
|
||||
|
||||
// Enqueue for durable transmission via the platform-specific queue
|
||||
messageQueue.enqueue(packetId)
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "Failed to send data packet" }
|
||||
Logger.e(ex) { "Failed to enqueue message packet" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun favoriteNode(node: Node) {
|
||||
try {
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(node))
|
||||
radioController.favoriteNode(node.num)
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "Favorite node error" }
|
||||
}
|
||||
|
|
@ -93,9 +131,7 @@ constructor(
|
|||
|
||||
private suspend fun sendSharedContact(node: Node) {
|
||||
try {
|
||||
val contact =
|
||||
SharedContact(node_num = node.num, user = node.user, manually_verified = node.manuallyVerified)
|
||||
serviceRepository.onServiceAction(ServiceAction.SendContact(contact = contact))
|
||||
radioController.sendSharedContact(node.num)
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "Send shared contact error" }
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for performing administrative actions on the radio. */
|
||||
open class AdminActionsUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val radioController: RadioController,
|
||||
private val nodeRepository: NodeRepository,
|
||||
) {
|
||||
/**
|
||||
* Reboots the radio.
|
||||
*
|
||||
* @param destNum The node number to reboot.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun reboot(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.reboot(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the radio.
|
||||
*
|
||||
* @param destNum The node number to shut down.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun shutdown(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.shutdown(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory resets the radio.
|
||||
*
|
||||
* @param destNum The node number to reset.
|
||||
* @param isLocal Whether the reset is being performed on the locally connected node.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun factoryReset(destNum: Int, isLocal: Boolean): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.factoryReset(destNum, packetId)
|
||||
|
||||
if (isLocal) {
|
||||
// If it's the local node, we should also clear the phone's node database as it will be out of sync.
|
||||
nodeRepository.clearNodeDB()
|
||||
}
|
||||
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the NodeDB on the radio.
|
||||
*
|
||||
* @param destNum The node number to reset.
|
||||
* @param preserveFavorites Whether to keep favorite nodes in the database.
|
||||
* @param isLocal Whether the reset is being performed on the locally connected node.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun nodedbReset(destNum: Int, preserveFavorites: Boolean, isLocal: Boolean): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.nodedbReset(destNum, packetId, preserveFavorites)
|
||||
|
||||
if (isLocal) {
|
||||
// If it's the local node, we should also clear the phone's node database.
|
||||
nodeRepository.clearNodeDB(preserveFavorites)
|
||||
}
|
||||
|
||||
return packetId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/** Use case for cleaning up nodes from the database. */
|
||||
class CleanNodeDatabaseUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val radioController: RadioController,
|
||||
) {
|
||||
/** Identifies nodes that match the cleanup criteria. */
|
||||
suspend fun getNodesToClean(olderThanDays: Float, onlyUnknownNodes: Boolean, currentTimeSeconds: Long): List<Node> {
|
||||
val sevenDaysAgoSeconds = currentTimeSeconds - 7.days.inWholeSeconds
|
||||
val olderThanTimestamp = currentTimeSeconds - olderThanDays.toInt().days.inWholeSeconds
|
||||
|
||||
val nodesToConsider =
|
||||
if (onlyUnknownNodes) {
|
||||
val olderNodes = nodeRepository.getNodesOlderThan(olderThanTimestamp.toInt())
|
||||
val unknownNodes = nodeRepository.getUnknownNodes()
|
||||
olderNodes.filter { itNode -> unknownNodes.any { it.num == itNode.num } }
|
||||
} else {
|
||||
nodeRepository.getNodesOlderThan(olderThanTimestamp.toInt())
|
||||
}
|
||||
|
||||
return nodesToConsider
|
||||
.filterNot { node ->
|
||||
(node.hasPKC && node.lastHeard >= sevenDaysAgoSeconds) || node.isIgnored || node.isFavorite
|
||||
}
|
||||
.map { it.toModel() }
|
||||
}
|
||||
|
||||
/** Performs the cleanup of specified nodes. */
|
||||
suspend fun cleanNodes(nodeNums: List<Int>) {
|
||||
if (nodeNums.isEmpty()) return
|
||||
|
||||
nodeRepository.deleteNodes(nodeNums)
|
||||
val packetId = radioController.getPacketId()
|
||||
for (nodeNum in nodeNums) {
|
||||
radioController.removeByNodenum(packetId, nodeNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.positionToMeter
|
||||
import org.meshtastic.proto.PortNum
|
||||
import java.io.BufferedWriter
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
/** Use case for exporting persisted packet data to a CSV format. */
|
||||
class ExportDataUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
) {
|
||||
/**
|
||||
* Writes all persisted packet data to the provided [BufferedWriter].
|
||||
*
|
||||
* @param writer The writer to output the CSV data to.
|
||||
* @param myNodeNum The node number of the current device.
|
||||
* @param filterPortnum If provided, only packets with this port number will be exported.
|
||||
*/
|
||||
@Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod", "detekt:NestedBlockDepth")
|
||||
suspend operator fun invoke(writer: BufferedWriter, myNodeNum: Int, filterPortnum: Int? = null) {
|
||||
val nodes = nodeRepository.nodeDBbyNum.value
|
||||
val positionToPos: (ProtoPosition?) -> Position? = { meshPosition ->
|
||||
meshPosition?.let { Position(it) }?.takeIf { it.isValid() }
|
||||
}
|
||||
|
||||
val nodePositions = mutableMapOf<Int, ProtoPosition?>()
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
writer.appendLine(
|
||||
"\"date\",\"time\",\"from\",\"sender name\",\"sender lat\",\"sender long\",\"rx lat\",\"rx long\",\"rx elevation\",\"rx snr\",\"distance(m)\",\"hop limit\",\"payload\"",
|
||||
)
|
||||
|
||||
val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
|
||||
meshLogRepository.getAllLogsInReceiveOrder(Int.MAX_VALUE).first().forEach { packet ->
|
||||
packet.nodeInfo?.let { nodeInfo ->
|
||||
positionToPos.invoke(nodeInfo.position)?.let { nodePositions[nodeInfo.num] = nodeInfo.position }
|
||||
}
|
||||
|
||||
packet.meshPacket?.let { proto ->
|
||||
packet.position?.let { position ->
|
||||
positionToPos.invoke(position)?.let {
|
||||
nodePositions[proto.from.takeIf { it != 0 } ?: myNodeNum] = position
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(filterPortnum == null || (proto.decoded?.portnum?.value ?: 0) == filterPortnum) &&
|
||||
proto.rx_snr != 0.0f
|
||||
) {
|
||||
val rxDateTime = dateFormat.format(packet.received_date)
|
||||
val rxFrom = proto.from.toUInt()
|
||||
val senderName = nodes[proto.from]?.user?.long_name ?: ""
|
||||
|
||||
val senderPosition = nodePositions[proto.from]
|
||||
val senderPos = positionToPos.invoke(senderPosition)
|
||||
val senderLat = senderPos?.latitude ?: ""
|
||||
val senderLong = senderPos?.longitude ?: ""
|
||||
|
||||
val rxPosition = nodePositions[myNodeNum]
|
||||
val rxPos = positionToPos.invoke(rxPosition)
|
||||
val rxLat = rxPos?.latitude ?: ""
|
||||
val rxLong = rxPos?.longitude ?: ""
|
||||
val rxAlt = rxPos?.altitude ?: ""
|
||||
val rxSnr = proto.rx_snr
|
||||
|
||||
val dist =
|
||||
if (senderPos == null || rxPos == null) {
|
||||
""
|
||||
} else {
|
||||
positionToMeter(Position(rxPosition!!), Position(senderPosition!!)).roundToInt().toString()
|
||||
}
|
||||
|
||||
val hopLimit = proto.hop_limit
|
||||
val decoded = proto.decoded
|
||||
val encrypted = proto.encrypted
|
||||
val payload =
|
||||
when {
|
||||
(decoded?.portnum?.value ?: 0) !in
|
||||
setOf(PortNum.TEXT_MESSAGE_APP.value, PortNum.RANGE_TEST_APP.value) ->
|
||||
"<${decoded?.portnum}>"
|
||||
|
||||
decoded != null -> decoded.payload.utf8().replace("\"", "\"\"")
|
||||
encrypted != null -> "${encrypted.size} encrypted bytes"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
writer.appendLine(
|
||||
"$rxDateTime,\"$rxFrom\",\"$senderName\",\"$senderLat\",\"$senderLong\",\"$rxLat\",\"$rxLong\",\"$rxAlt\",\"$rxSnr\",\"$dist\",\"$hopLimit\",\"$payload\"",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for exporting a device profile to an output stream. */
|
||||
class ExportProfileUseCase @Inject constructor() {
|
||||
/**
|
||||
* Exports the provided [DeviceProfile] to the given [OutputStream].
|
||||
*
|
||||
* @param outputStream The stream to write the profile to.
|
||||
* @param profile The device profile to export.
|
||||
* @return A [Result] indicating success or failure.
|
||||
*/
|
||||
operator fun invoke(outputStream: OutputStream, profile: DeviceProfile): Result<Unit> = runCatching {
|
||||
outputStream.write(profile.encode())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import android.util.Base64
|
||||
import org.json.JSONObject
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.proto.Config
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for exporting security configuration to a JSON format. */
|
||||
class ExportSecurityConfigUseCase @Inject constructor() {
|
||||
/**
|
||||
* Exports the provided [Config.SecurityConfig] as a JSON string to the given [OutputStream].
|
||||
*
|
||||
* @param outputStream The stream to write the JSON to.
|
||||
* @param securityConfig The security configuration to export.
|
||||
* @return A [Result] indicating success or failure.
|
||||
*/
|
||||
operator fun invoke(outputStream: OutputStream, securityConfig: Config.SecurityConfig): Result<Unit> = runCatching {
|
||||
val publicKeyBytes = securityConfig.public_key.toByteArray()
|
||||
val privateKeyBytes = securityConfig.private_key.toByteArray()
|
||||
|
||||
// Convert byte arrays to Base64 strings
|
||||
val publicKeyBase64 = Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
||||
val privateKeyBase64 = Base64.encodeToString(privateKeyBytes, Base64.NO_WRAP)
|
||||
|
||||
// Create a JSON object
|
||||
val jsonObject =
|
||||
JSONObject().apply {
|
||||
put("timestamp", nowMillis)
|
||||
put("public_key", publicKeyBase64)
|
||||
put("private_key", privateKeyBase64)
|
||||
}
|
||||
|
||||
val jsonString = jsonObject.toString(JSON_INDENT_SPACES)
|
||||
outputStream.write(jsonString.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val JSON_INDENT_SPACES = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for importing a device profile from an input stream. */
|
||||
class ImportProfileUseCase @Inject constructor() {
|
||||
/**
|
||||
* Imports a [DeviceProfile] from the provided [InputStream].
|
||||
*
|
||||
* @param inputStream The stream to read the profile from.
|
||||
* @return A [Result] containing the imported [DeviceProfile] or an error.
|
||||
*/
|
||||
operator fun invoke(inputStream: InputStream): Result<DeviceProfile> = runCatching {
|
||||
val bytes = inputStream.readBytes()
|
||||
DeviceProfile.ADAPTER.decode(bytes)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for installing a device profile onto a radio. */
|
||||
class InstallProfileUseCase @Inject constructor(private val radioController: RadioController) {
|
||||
/**
|
||||
* Installs the provided [DeviceProfile] onto the radio at [destNum].
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param profile The device profile to install.
|
||||
* @param currentUser The current user configuration of the destination node (to preserve names if not in profile).
|
||||
*/
|
||||
suspend operator fun invoke(destNum: Int, profile: DeviceProfile, currentUser: User?) {
|
||||
radioController.beginEditSettings(destNum)
|
||||
|
||||
installOwner(destNum, profile, currentUser)
|
||||
installConfig(destNum, profile.config)
|
||||
installFixedPosition(destNum, profile.fixed_position)
|
||||
installModuleConfig(destNum, profile.module_config)
|
||||
|
||||
radioController.commitEditSettings(destNum)
|
||||
}
|
||||
|
||||
private suspend fun installOwner(destNum: Int, profile: DeviceProfile, currentUser: User?) {
|
||||
if (profile.long_name != null || profile.short_name != null) {
|
||||
currentUser?.let {
|
||||
val user =
|
||||
it.copy(
|
||||
long_name = profile.long_name ?: it.long_name,
|
||||
short_name = profile.short_name ?: it.short_name,
|
||||
)
|
||||
radioController.setOwner(destNum, user, radioController.getPacketId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installConfig(destNum: Int, config: LocalConfig?) {
|
||||
config?.let { lc ->
|
||||
lc.device?.let { radioController.setConfig(destNum, Config(device = it), radioController.getPacketId()) }
|
||||
lc.position?.let {
|
||||
radioController.setConfig(destNum, Config(position = it), radioController.getPacketId())
|
||||
}
|
||||
lc.power?.let { radioController.setConfig(destNum, Config(power = it), radioController.getPacketId()) }
|
||||
lc.network?.let { radioController.setConfig(destNum, Config(network = it), radioController.getPacketId()) }
|
||||
lc.display?.let { radioController.setConfig(destNum, Config(display = it), radioController.getPacketId()) }
|
||||
lc.lora?.let { radioController.setConfig(destNum, Config(lora = it), radioController.getPacketId()) }
|
||||
lc.bluetooth?.let {
|
||||
radioController.setConfig(destNum, Config(bluetooth = it), radioController.getPacketId())
|
||||
}
|
||||
lc.security?.let {
|
||||
radioController.setConfig(destNum, Config(security = it), radioController.getPacketId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installFixedPosition(destNum: Int, fixedPosition: org.meshtastic.proto.Position?) {
|
||||
if (fixedPosition != null) {
|
||||
radioController.setFixedPosition(destNum, Position(fixedPosition))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installModuleConfig(destNum: Int, moduleConfig: LocalModuleConfig?) {
|
||||
moduleConfig?.let { lmc ->
|
||||
installModuleConfigPart1(destNum, lmc)
|
||||
installModuleConfigPart2(destNum, lmc)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installModuleConfigPart1(destNum: Int, lmc: LocalModuleConfig) {
|
||||
lmc.mqtt?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(mqtt = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.serial?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(serial = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.external_notification?.let {
|
||||
radioController.setModuleConfig(
|
||||
destNum,
|
||||
ModuleConfig(external_notification = it),
|
||||
radioController.getPacketId(),
|
||||
)
|
||||
}
|
||||
lmc.store_forward?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(store_forward = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.range_test?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(range_test = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.telemetry?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(telemetry = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.canned_message?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(canned_message = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.audio?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(audio = it), radioController.getPacketId())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun installModuleConfigPart2(destNum: Int, lmc: LocalModuleConfig) {
|
||||
lmc.remote_hardware?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(remote_hardware = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.neighbor_info?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(neighbor_info = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.ambient_lighting?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(ambient_lighting = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.detection_sensor?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(detection_sensor = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.paxcounter?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(paxcounter = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.statusmessage?.let {
|
||||
radioController.setModuleConfig(destNum, ModuleConfig(statusmessage = it), radioController.getPacketId())
|
||||
}
|
||||
lmc.traffic_management?.let {
|
||||
radioController.setModuleConfig(
|
||||
destNum,
|
||||
ModuleConfig(traffic_management = it),
|
||||
radioController.getPacketId(),
|
||||
)
|
||||
}
|
||||
lmc.tak?.let { radioController.setModuleConfig(destNum, ModuleConfig(tak = it), radioController.getPacketId()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
import org.meshtastic.core.prefs.radio.isBle
|
||||
import org.meshtastic.core.prefs.radio.isSerial
|
||||
import org.meshtastic.core.prefs.radio.isTcp
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case to determine if the currently connected device is capable of over-the-air (OTA) updates. */
|
||||
class IsOtaCapableUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val radioController: RadioController,
|
||||
private val radioPrefs: RadioPrefs,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<Boolean> = combine(nodeRepository.ourNodeInfo, radioController.connectionState) {
|
||||
node: Node?,
|
||||
connectionState: ConnectionState,
|
||||
->
|
||||
node to connectionState
|
||||
}
|
||||
.flatMapLatest { (node, connectionState) ->
|
||||
if (node == null || connectionState != ConnectionState.Connected) {
|
||||
flowOf(false)
|
||||
} else if (radioPrefs.isBle() || radioPrefs.isSerial() || radioPrefs.isTcp()) {
|
||||
val hwModel = node.user.hw_model.value
|
||||
val hw = deviceHardwareRepository.getDeviceHardwareByModel(hwModel).getOrNull()
|
||||
|
||||
// ESP32 Unified OTA is only supported via BLE or WiFi (TCP), not USB Serial.
|
||||
// TODO: Re-enable when supportsUnifiedOta is added to DeviceHardware
|
||||
val isEsp32OtaSupported = false
|
||||
|
||||
flowOf(hw?.requiresDfu == true || isEsp32OtaSupported)
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for controlling location sharing with the mesh. */
|
||||
class MeshLocationUseCase @Inject constructor(private val radioController: RadioController) {
|
||||
/** Starts providing the phone's location to the mesh. */
|
||||
fun startProvidingLocation() {
|
||||
radioController.startProvideLocation()
|
||||
}
|
||||
|
||||
/** Stops providing the phone's location to the mesh. */
|
||||
fun stopProvidingLocation() {
|
||||
radioController.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.core.database.model.getStringResFrom
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.DeviceConnectionStatus
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Routing
|
||||
import org.meshtastic.proto.User
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Sealed class representing the result of processing a radio response packet. */
|
||||
sealed class RadioResponseResult {
|
||||
data class Metadata(val metadata: DeviceMetadata) : RadioResponseResult()
|
||||
|
||||
data class ChannelResponse(val channel: Channel) : RadioResponseResult()
|
||||
|
||||
data class Owner(val user: User) : RadioResponseResult()
|
||||
|
||||
data class ConfigResponse(val config: org.meshtastic.proto.Config) : RadioResponseResult()
|
||||
|
||||
data class ModuleConfigResponse(val config: org.meshtastic.proto.ModuleConfig) : RadioResponseResult()
|
||||
|
||||
data class CannedMessages(val messages: String) : RadioResponseResult()
|
||||
|
||||
data class Ringtone(val ringtone: String) : RadioResponseResult()
|
||||
|
||||
data class ConnectionStatus(val status: DeviceConnectionStatus) : RadioResponseResult()
|
||||
|
||||
data class Error(val message: UiText) : RadioResponseResult()
|
||||
|
||||
data object Success : RadioResponseResult()
|
||||
}
|
||||
|
||||
/** Use case for processing incoming [MeshPacket]s that are responses to admin requests. */
|
||||
class ProcessRadioResponseUseCase @Inject constructor() {
|
||||
/**
|
||||
* Decodes and processes the provided [packet].
|
||||
*
|
||||
* @param packet The mesh packet received from the radio.
|
||||
* @param destNum The node number that the response is expected from.
|
||||
* @param requestIds The set of active request IDs.
|
||||
* @return A [RadioResponseResult] if the packet matches a request, or null otherwise.
|
||||
*/
|
||||
@Suppress("CyclomaticComplexMethod", "NestedBlockDepth")
|
||||
operator fun invoke(packet: MeshPacket, destNum: Int, requestIds: Set<Int>): RadioResponseResult? {
|
||||
val data = packet.decoded
|
||||
if (data == null || data.request_id !in requestIds) {
|
||||
return null
|
||||
}
|
||||
|
||||
return when (data.portnum) {
|
||||
PortNum.ROUTING_APP -> processRoutingResponse(packet, data, destNum)
|
||||
PortNum.ADMIN_APP -> processAdminResponse(packet, data, destNum)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRoutingResponse(packet: MeshPacket, data: Data, destNum: Int): RadioResponseResult? {
|
||||
val parsed = Routing.ADAPTER.decode(data.payload)
|
||||
return when {
|
||||
parsed.error_reason != Routing.Error.NONE ->
|
||||
RadioResponseResult.Error(UiText.Resource(getStringResFrom(parsed.error_reason?.value ?: 0)))
|
||||
packet.from == destNum -> RadioResponseResult.Success
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun processAdminResponse(packet: MeshPacket, data: Data, destNum: Int): RadioResponseResult {
|
||||
if (destNum != packet.from) {
|
||||
return RadioResponseResult.Error(
|
||||
UiText.DynamicString("Unexpected sender: ${packet.from.toUInt()} instead of ${destNum.toUInt()}."),
|
||||
)
|
||||
}
|
||||
|
||||
val parsed = AdminMessage.ADAPTER.decode(data.payload)
|
||||
return processAdminMessage(parsed)
|
||||
}
|
||||
|
||||
private fun processAdminMessage(parsed: AdminMessage): RadioResponseResult = when {
|
||||
parsed.get_device_metadata_response != null ->
|
||||
RadioResponseResult.Metadata(parsed.get_device_metadata_response!!)
|
||||
|
||||
parsed.get_channel_response != null -> RadioResponseResult.ChannelResponse(parsed.get_channel_response!!)
|
||||
|
||||
parsed.get_owner_response != null -> RadioResponseResult.Owner(parsed.get_owner_response!!)
|
||||
|
||||
parsed.get_config_response != null -> RadioResponseResult.ConfigResponse(parsed.get_config_response!!)
|
||||
|
||||
parsed.get_module_config_response != null ->
|
||||
RadioResponseResult.ModuleConfigResponse(parsed.get_module_config_response!!)
|
||||
|
||||
parsed.get_canned_message_module_messages_response != null ->
|
||||
RadioResponseResult.CannedMessages(parsed.get_canned_message_module_messages_response!!)
|
||||
|
||||
parsed.get_ringtone_response != null -> RadioResponseResult.Ringtone(parsed.get_ringtone_response!!)
|
||||
|
||||
parsed.get_device_connection_status_response != null ->
|
||||
RadioResponseResult.ConnectionStatus(parsed.get_device_connection_status_response!!)
|
||||
|
||||
else -> {
|
||||
Logger.d { "No custom processing needed for $parsed" }
|
||||
RadioResponseResult.Success
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for interacting with radio configuration components. */
|
||||
@Suppress("TooManyFunctions")
|
||||
open class RadioConfigUseCase @Inject constructor(private val radioController: RadioController) {
|
||||
/**
|
||||
* Updates the owner information on the radio.
|
||||
*
|
||||
* @param destNum The node number to update.
|
||||
* @param user The new user configuration.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun setOwner(destNum: Int, user: User): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.setOwner(destNum, user, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the owner information from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getOwner(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getOwner(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a configuration section on the radio.
|
||||
*
|
||||
* @param destNum The node number to update.
|
||||
* @param config The new configuration.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun setConfig(destNum: Int, config: Config): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.setConfig(destNum, config, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a configuration section from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @param configType The type of configuration to request (from [org.meshtastic.proto.AdminMessage.ConfigType]).
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getConfig(destNum: Int, configType: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getConfig(destNum, configType, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a module configuration section on the radio.
|
||||
*
|
||||
* @param destNum The node number to update.
|
||||
* @param config The new module configuration.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun setModuleConfig(destNum: Int, config: ModuleConfig): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.setModuleConfig(destNum, config, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a module configuration section from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @param moduleConfigType The type of module configuration to request.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getModuleConfig(destNum, moduleConfigType, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a channel from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @param index The index of the channel to request.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getChannel(destNum: Int, index: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getChannel(destNum, index, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a channel on the radio.
|
||||
*
|
||||
* @param destNum The node number to update.
|
||||
* @param channel The new channel configuration.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.setRemoteChannel(destNum, channel, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/** Updates the fixed position on the radio. */
|
||||
suspend fun setFixedPosition(destNum: Int, position: Position) {
|
||||
radioController.setFixedPosition(destNum, position)
|
||||
}
|
||||
|
||||
/** Removes the fixed position on the radio. */
|
||||
suspend fun removeFixedPosition(destNum: Int) {
|
||||
radioController.setFixedPosition(destNum, Position(0.0, 0.0, 0))
|
||||
}
|
||||
|
||||
/** Sets the ringtone on the radio. */
|
||||
suspend fun setRingtone(destNum: Int, ringtone: String) {
|
||||
radioController.setRingtone(destNum, ringtone)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the ringtone from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getRingtone(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getRingtone(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/** Sets the canned messages on the radio. */
|
||||
suspend fun setCannedMessages(destNum: Int, messages: String) {
|
||||
radioController.setCannedMessages(destNum, messages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the canned messages from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getCannedMessages(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getCannedMessages(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the device connection status from the radio.
|
||||
*
|
||||
* @param destNum The node number to query.
|
||||
* @return The packet ID of the request.
|
||||
*/
|
||||
suspend fun getDeviceConnectionStatus(destNum: Int): Int {
|
||||
val packetId = radioController.getPacketId()
|
||||
radioController.getDeviceConnectionStatus(destNum, packetId)
|
||||
return packetId
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for setting whether the application intro has been completed. */
|
||||
class SetAppIntroCompletedUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
|
||||
operator fun invoke(completed: Boolean) {
|
||||
uiPreferencesDataSource.setAppIntroCompleted(completed)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for setting the database cache limit. */
|
||||
class SetDatabaseCacheLimitUseCase @Inject constructor(private val databaseManager: DatabaseManager) {
|
||||
operator fun invoke(limit: Int) {
|
||||
val clamped = limit.coerceIn(DatabaseConstants.MIN_CACHE_LIMIT, DatabaseConstants.MAX_CACHE_LIMIT)
|
||||
databaseManager.setCacheLimit(clamped)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for managing mesh log settings. */
|
||||
class SetMeshLogSettingsUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val meshLogPrefs: MeshLogPrefs,
|
||||
) {
|
||||
/**
|
||||
* Sets the retention period for mesh logs.
|
||||
*
|
||||
* @param days The number of days to retain logs.
|
||||
*/
|
||||
suspend fun setRetentionDays(days: Int) {
|
||||
val clamped = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS)
|
||||
meshLogPrefs.retentionDays = clamped
|
||||
meshLogRepository.deleteLogsOlderThan(clamped)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables mesh logging.
|
||||
*
|
||||
* @param enabled True to enable logging, false to disable.
|
||||
*/
|
||||
suspend fun setLoggingEnabled(enabled: Boolean) {
|
||||
meshLogPrefs.loggingEnabled = enabled
|
||||
if (!enabled) {
|
||||
meshLogRepository.deleteAll()
|
||||
} else {
|
||||
meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for setting whether to provide the node location to the mesh. */
|
||||
class SetProvideLocationUseCase @Inject constructor(private val uiPrefs: UiPrefs) {
|
||||
operator fun invoke(myNodeNum: Int, provideLocation: Boolean) {
|
||||
uiPrefs.setShouldProvideNodeLocation(myNodeNum, provideLocation)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for setting the application theme. */
|
||||
class SetThemeUseCase @Inject constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
|
||||
operator fun invoke(themeMode: Int) {
|
||||
uiPreferencesDataSource.setTheme(themeMode)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for toggling the analytics preference. */
|
||||
class ToggleAnalyticsUseCase @Inject constructor(private val analyticsPrefs: AnalyticsPrefs) {
|
||||
operator fun invoke() {
|
||||
analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Use case for toggling the homoglyph encoding preference. */
|
||||
class ToggleHomoglyphEncodingUseCase @Inject constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) {
|
||||
operator fun invoke() {
|
||||
homoglyphEncodingPrefs.homoglyphEncodingEnabled = !homoglyphEncodingPrefs.homoglyphEncodingEnabled
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
|
||||
class FakeRadioController : RadioController {
|
||||
|
||||
// Mutable state flows so we can manipulate them in our tests
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Connected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
private val _clientNotification = MutableStateFlow<ClientNotification?>(null)
|
||||
override val clientNotification: StateFlow<ClientNotification?> = _clientNotification
|
||||
|
||||
// Track sent packets to assert in tests
|
||||
val sentPackets = mutableListOf<DataPacket>()
|
||||
val favoritedNodes = mutableListOf<Int>()
|
||||
val sentSharedContacts = mutableListOf<Int>()
|
||||
|
||||
override suspend fun sendMessage(packet: DataPacket) {
|
||||
sentPackets.add(packet)
|
||||
}
|
||||
|
||||
override fun clearClientNotification() {
|
||||
_clientNotification.value = null
|
||||
}
|
||||
|
||||
override suspend fun favoriteNode(nodeNum: Int) {
|
||||
favoritedNodes.add(nodeNum)
|
||||
}
|
||||
|
||||
override suspend fun sendSharedContact(nodeNum: Int) {
|
||||
sentSharedContacts.add(nodeNum)
|
||||
}
|
||||
|
||||
override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) {}
|
||||
|
||||
override suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int) {}
|
||||
|
||||
override suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int) {}
|
||||
|
||||
override suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int) {}
|
||||
|
||||
override suspend fun setFixedPosition(destNum: Int, position: org.meshtastic.core.model.Position) {}
|
||||
|
||||
override suspend fun setRingtone(destNum: Int, ringtone: String) {}
|
||||
|
||||
override suspend fun setCannedMessages(destNum: Int, messages: String) {}
|
||||
|
||||
override suspend fun getOwner(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getChannel(destNum: Int, index: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getRingtone(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getCannedMessages(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun reboot(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun shutdown(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun factoryReset(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean) {}
|
||||
|
||||
override suspend fun removeByNodenum(packetId: Int, nodeNum: Int) {}
|
||||
|
||||
override suspend fun beginEditSettings(destNum: Int) {}
|
||||
|
||||
override suspend fun commitEditSettings(destNum: Int) {}
|
||||
|
||||
override fun getPacketId(): Int = 1
|
||||
|
||||
override fun startProvideLocation() {}
|
||||
|
||||
override fun stopProvideLocation() {}
|
||||
|
||||
// --- Helper methods for testing ---
|
||||
|
||||
fun setConnectionState(state: ConnectionState) {
|
||||
_connectionState.value = state
|
||||
}
|
||||
}
|
||||
|
|
@ -14,49 +14,67 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.usecase
|
||||
package org.meshtastic.core.domain.usecase
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.domain.FakeRadioController
|
||||
import org.meshtastic.core.domain.MessageQueue
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
|
||||
class SendMessageUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var serviceRepository: ServiceRepository
|
||||
private lateinit var packetRepository: PacketRepository
|
||||
private lateinit var radioController: FakeRadioController
|
||||
private lateinit var homoglyphEncodingPrefs: HomoglyphPrefs
|
||||
private lateinit var messageQueue: MessageQueue
|
||||
private lateinit var useCase: SendMessageUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeRepository = mockk(relaxed = true)
|
||||
serviceRepository = mockk(relaxed = true)
|
||||
packetRepository = mockk(relaxed = true)
|
||||
radioController = FakeRadioController()
|
||||
homoglyphEncodingPrefs = mockk(relaxed = true)
|
||||
messageQueue = mockk(relaxed = true)
|
||||
|
||||
useCase =
|
||||
SendMessageUseCase(
|
||||
nodeRepository = nodeRepository,
|
||||
serviceRepository = serviceRepository,
|
||||
packetRepository = packetRepository,
|
||||
radioController = radioController,
|
||||
homoglyphEncodingPrefs = homoglyphEncodingPrefs,
|
||||
messageQueue = messageQueue,
|
||||
)
|
||||
|
||||
mockkConstructor(Capabilities::class)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with broadcast message simply sends data packet`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -69,8 +87,11 @@ class SendMessageUseCaseTest {
|
|||
useCase("Hello broadcast", "0${DataPacket.ID_BROADCAST}", null)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { serviceRepository.onServiceAction(any()) }
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(any()) }
|
||||
assertEquals(0, radioController.favoritedNodes.size)
|
||||
assertEquals(0, radioController.sentSharedContacts.size)
|
||||
|
||||
coVerify { packetRepository.insert(any<Packet>()) }
|
||||
coVerify { messageQueue.enqueue(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -86,18 +107,21 @@ class SendMessageUseCaseTest {
|
|||
|
||||
val destNode = mockk<Node>(relaxed = true)
|
||||
every { destNode.isFavorite } returns false
|
||||
every { destNode.num } returns 12345
|
||||
every { nodeRepository.getNode("!dest") } returns destNode
|
||||
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false
|
||||
|
||||
every { anyConstructed<Capabilities>().canSendVerifiedContacts } returns false
|
||||
|
||||
// Act
|
||||
useCase("Direct message", "!dest", null)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { serviceRepository.onServiceAction(match { it is ServiceAction.Favorite }) }
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(any()) }
|
||||
assertEquals(1, radioController.favoritedNodes.size)
|
||||
assertEquals(12345, radioController.favoritedNodes[0])
|
||||
|
||||
coVerify { packetRepository.insert(any<Packet>()) }
|
||||
coVerify { messageQueue.enqueue(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -112,18 +136,21 @@ class SendMessageUseCaseTest {
|
|||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
|
||||
val destNode = mockk<Node>(relaxed = true)
|
||||
every { destNode.num } returns 67890
|
||||
every { nodeRepository.getNode("!dest") } returns destNode
|
||||
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false
|
||||
|
||||
every { anyConstructed<Capabilities>().canSendVerifiedContacts } returns true
|
||||
|
||||
// Act
|
||||
useCase("Direct message", "!dest", null)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { serviceRepository.onServiceAction(match { it is ServiceAction.SendContact }) }
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(any()) }
|
||||
assertEquals(1, radioController.sentSharedContacts.size)
|
||||
assertEquals(67890, radioController.sentSharedContacts[0])
|
||||
|
||||
coVerify { packetRepository.insert(any<Packet>()) }
|
||||
coVerify { messageQueue.enqueue(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -133,14 +160,15 @@ class SendMessageUseCaseTest {
|
|||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns true
|
||||
|
||||
// Let's use a cyrillic character 'A' (U+0410) that will be mapped to Latin 'A'
|
||||
val originalText = "\u0410pple"
|
||||
val originalText = "\u0410pple" // Cyrillic A
|
||||
|
||||
// Act
|
||||
useCase(originalText, "0${DataPacket.ID_BROADCAST}", null)
|
||||
|
||||
// Assert
|
||||
// We verify that send was called with the transformed text (Latin 'A'pple)
|
||||
coVerify(exactly = 1) { serviceRepository.meshService?.send(match { it.text?.contains("Apple") == true }) }
|
||||
val packetSlot = slot<Packet>()
|
||||
coVerify { packetRepository.insert(capture(packetSlot)) }
|
||||
assertTrue(packetSlot.captured.data?.text?.contains("Apple") == true)
|
||||
coVerify { messageQueue.enqueue(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.model.RadioController
|
||||
|
||||
class AdminActionsUseCaseTest {
|
||||
|
||||
private lateinit var radioController: RadioController
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var useCase: AdminActionsUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
radioController = mockk(relaxed = true)
|
||||
nodeRepository = mockk(relaxed = true)
|
||||
useCase = AdminActionsUseCase(radioController, nodeRepository)
|
||||
every { radioController.getPacketId() } returns 42
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reboot calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.reboot(123)
|
||||
coVerify { radioController.reboot(123, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shutdown calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.shutdown(123)
|
||||
coVerify { radioController.shutdown(123, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `factoryReset calls radioController and clears DB if local`() = runTest {
|
||||
val result = useCase.factoryReset(123, isLocal = true)
|
||||
coVerify { radioController.factoryReset(123, 42) }
|
||||
coVerify { nodeRepository.clearNodeDB() }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nodedbReset calls radioController and clears DB if local`() = runTest {
|
||||
val result = useCase.nodedbReset(123, preserveFavorites = true, isLocal = true)
|
||||
coVerify { radioController.nodedbReset(123, 42, true) }
|
||||
coVerify { nodeRepository.clearNodeDB(true) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.domain.FakeRadioController
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
class CleanNodeDatabaseUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var radioController: FakeRadioController
|
||||
private lateinit var useCase: CleanNodeDatabaseUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeRepository = mockk(relaxed = true)
|
||||
radioController = FakeRadioController()
|
||||
useCase = CleanNodeDatabaseUseCase(nodeRepository, radioController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getNodesToClean filters nodes correctly`() = runTest {
|
||||
// Arrange
|
||||
val currentTime = 1000000L
|
||||
val olderThanTimestamp = currentTime - 30.days.inWholeSeconds
|
||||
|
||||
val oldNode = NodeEntity(num = 1, lastHeard = (olderThanTimestamp - 1).toInt())
|
||||
val newNode = NodeEntity(num = 2, lastHeard = (currentTime - 1).toInt())
|
||||
val ignoredNode = NodeEntity(num = 3, lastHeard = (olderThanTimestamp - 1).toInt(), isIgnored = true)
|
||||
|
||||
coEvery { nodeRepository.getNodesOlderThan(any()) } returns listOf(oldNode, ignoredNode)
|
||||
|
||||
// Act
|
||||
val result = useCase.getNodesToClean(30f, false, currentTime)
|
||||
|
||||
// Assert
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(1, result[0].num)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cleanNodes calls repository and controller`() = runTest {
|
||||
// Act
|
||||
useCase.cleanNodes(listOf(1, 2))
|
||||
|
||||
// Assert
|
||||
coVerify { nodeRepository.deleteNodes(listOf(1, 2)) }
|
||||
// Note: we can't easily verify removeByNodenum on FakeRadioController without adding tracking
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.User
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.io.BufferedWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class ExportDataUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var meshLogRepository: MeshLogRepository
|
||||
private lateinit var useCase: ExportDataUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeRepository = mockk(relaxed = true)
|
||||
meshLogRepository = mockk(relaxed = true)
|
||||
useCase = ExportDataUseCase(nodeRepository, meshLogRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke writes header and log data`() = runTest {
|
||||
// Arrange
|
||||
val myNodeNum = 123
|
||||
val senderNodeNum = 456
|
||||
val senderNode = Node(num = senderNodeNum, user = User(long_name = "Sender Name"))
|
||||
|
||||
val nodes = mapOf(senderNodeNum to senderNode)
|
||||
val stateFlow = MutableStateFlow(nodes)
|
||||
every { nodeRepository.nodeDBbyNum } returns stateFlow
|
||||
every { nodeRepository.getNodeEntityDBbyNumFlow() } returns flowOf(emptyMap())
|
||||
|
||||
val meshPacket =
|
||||
MeshPacket(
|
||||
from = senderNodeNum,
|
||||
rx_snr = 5.5f,
|
||||
decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP, payload = "Hello".encodeUtf8()),
|
||||
)
|
||||
val meshLog =
|
||||
MeshLog(
|
||||
uuid = "uuid-1",
|
||||
message_type = "Packet",
|
||||
received_date = 1700000000000L,
|
||||
raw_message = "",
|
||||
fromNum = senderNodeNum,
|
||||
portNum = PortNum.TEXT_MESSAGE_APP.value,
|
||||
fromRadio = FromRadio(packet = meshPacket),
|
||||
)
|
||||
every { meshLogRepository.getAllLogsInReceiveOrder(any()) } returns flowOf(listOf(meshLog))
|
||||
|
||||
val stringWriter = StringWriter()
|
||||
val bufferedWriter = BufferedWriter(stringWriter)
|
||||
|
||||
// Act
|
||||
useCase(bufferedWriter, myNodeNum)
|
||||
bufferedWriter.flush()
|
||||
|
||||
// Assert
|
||||
val output = stringWriter.toString()
|
||||
assertTrue("Header should be present", output.contains("\"date\",\"time\",\"from\",\"sender name\""))
|
||||
assertTrue("Sender name should be present", output.contains("Sender Name"))
|
||||
assertTrue("Payload should be present", output.contains("Hello"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class ExportProfileUseCaseTest {
|
||||
|
||||
private lateinit var useCase: ExportProfileUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
useCase = ExportProfileUseCase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke writes encoded profile to output stream`() {
|
||||
// Arrange
|
||||
val profile = DeviceProfile(long_name = "Export Node")
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
// Act
|
||||
val result = useCase(outputStream, profile)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
assertArrayEquals(profile.encode(), outputStream.toByteArray())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.proto.Config
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class ExportSecurityConfigUseCaseTest {
|
||||
|
||||
private lateinit var useCase: ExportSecurityConfigUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
useCase = ExportSecurityConfigUseCase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke writes valid JSON to output stream`() {
|
||||
// Arrange
|
||||
val publicKey = byteArrayOf(1, 2, 3).toByteString()
|
||||
val privateKey = byteArrayOf(4, 5, 6).toByteString()
|
||||
val config = Config.SecurityConfig(public_key = publicKey, private_key = privateKey)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
|
||||
// Act
|
||||
val result = useCase(outputStream, config)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
val json = JSONObject(outputStream.toString())
|
||||
assertTrue(json.has("timestamp"))
|
||||
assertTrue(json.has("public_key"))
|
||||
assertTrue(json.has("private_key"))
|
||||
// Check base64 values
|
||||
assertEquals("AQID", json.getString("public_key"))
|
||||
assertEquals("BAUG", json.getString("private_key"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class ImportProfileUseCaseTest {
|
||||
|
||||
private lateinit var useCase: ImportProfileUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
useCase = ImportProfileUseCase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with valid data returns profile`() {
|
||||
// Arrange
|
||||
val profile = DeviceProfile(long_name = "Test Node")
|
||||
val inputStream = ByteArrayInputStream(profile.encode())
|
||||
|
||||
// Act
|
||||
val result = useCase(inputStream)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals("Test Node", result.getOrNull()?.long_name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with invalid data returns failure`() {
|
||||
// Arrange
|
||||
val inputStream = ByteArrayInputStream(byteArrayOf(1, 2, 3))
|
||||
|
||||
// Act
|
||||
val result = useCase(inputStream)
|
||||
|
||||
// Assert
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
class InstallProfileUseCaseTest {
|
||||
|
||||
private lateinit var radioController: RadioController
|
||||
private lateinit var useCase: InstallProfileUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
radioController = mockk(relaxed = true)
|
||||
useCase = InstallProfileUseCase(radioController)
|
||||
every { radioController.getPacketId() } returns 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with names updates owner`() = runTest {
|
||||
// Arrange
|
||||
val profile = DeviceProfile(long_name = "New Long", short_name = "NL")
|
||||
val currentUser = User(long_name = "Old Long", short_name = "OL")
|
||||
|
||||
// Act
|
||||
useCase(123, profile, currentUser)
|
||||
|
||||
// Assert
|
||||
coVerify { radioController.beginEditSettings(123) }
|
||||
coVerify { radioController.setOwner(123, match { it.long_name == "New Long" && it.short_name == "NL" }, 1) }
|
||||
coVerify { radioController.commitEditSettings(123) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with config sets config`() = runTest {
|
||||
// Arrange
|
||||
val loraConfig = Config.LoRaConfig(region = Config.LoRaConfig.RegionCode.US)
|
||||
val profile = DeviceProfile(config = LocalConfig(lora = loraConfig))
|
||||
|
||||
// Act
|
||||
useCase(456, profile, null)
|
||||
|
||||
// Assert
|
||||
coVerify { radioController.setConfig(456, match { it.lora == loraConfig }, 1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with module_config sets module config`() = runTest {
|
||||
// Arrange
|
||||
val mqttConfig = ModuleConfig.MQTTConfig(enabled = true, address = "broker.local")
|
||||
val profile = DeviceProfile(module_config = LocalModuleConfig(mqtt = mqttConfig))
|
||||
|
||||
// Act
|
||||
useCase(789, profile, null)
|
||||
|
||||
// Assert
|
||||
coVerify { radioController.setModuleConfig(789, match { it.mqtt == mqttConfig }, 1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with module_config part 2 sets module config`() = runTest {
|
||||
// Arrange
|
||||
val neighborInfoConfig = ModuleConfig.NeighborInfoConfig(enabled = true)
|
||||
val profile = DeviceProfile(module_config = LocalModuleConfig(neighbor_info = neighborInfoConfig))
|
||||
|
||||
// Act
|
||||
useCase(789, profile, null)
|
||||
|
||||
// Assert
|
||||
coVerify { radioController.setModuleConfig(789, match { it.neighbor_info == neighborInfoConfig }, 1) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import app.cash.turbine.test
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
|
||||
class IsOtaCapableUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var radioController: RadioController
|
||||
private lateinit var radioPrefs: RadioPrefs
|
||||
private lateinit var deviceHardwareRepository: DeviceHardwareRepository
|
||||
private lateinit var useCase: IsOtaCapableUseCase
|
||||
|
||||
private val ourNodeInfoFlow = MutableStateFlow<Node?>(null)
|
||||
private val connectionStateFlow = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeRepository = mockk { every { ourNodeInfo } returns ourNodeInfoFlow }
|
||||
radioController = mockk { every { connectionState } returns connectionStateFlow }
|
||||
radioPrefs = mockk(relaxed = true)
|
||||
deviceHardwareRepository = mockk(relaxed = true)
|
||||
|
||||
useCase = IsOtaCapableUseCase(nodeRepository, radioController, radioPrefs, deviceHardwareRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when node is null`() = runTest {
|
||||
ourNodeInfoFlow.value = null
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
|
||||
useCase().test {
|
||||
assertFalse(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when not connected`() = runTest {
|
||||
val node = mockk<Node>(relaxed = true)
|
||||
ourNodeInfoFlow.value = node
|
||||
connectionStateFlow.value = ConnectionState.Disconnected
|
||||
|
||||
useCase().test {
|
||||
assertFalse(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when radio is not BLE, Serial, or TCP`() = runTest {
|
||||
val node = mockk<Node>(relaxed = true)
|
||||
ourNodeInfoFlow.value = node
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
every { radioPrefs.devAddr } returns "m123" // Mock
|
||||
|
||||
useCase().test {
|
||||
assertFalse(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when hw requires Dfu`() = runTest {
|
||||
val node = mockk<Node>(relaxed = true)
|
||||
ourNodeInfoFlow.value = node
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
every { radioPrefs.devAddr } returns "x123" // BLE
|
||||
|
||||
val hw = mockk<org.meshtastic.core.model.DeviceHardware> { every { requiresDfu } returns true }
|
||||
coEvery { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw)
|
||||
|
||||
useCase().test {
|
||||
assertTrue(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when hw does not require Dfu and isEsp32OtaSupported is false`() = runTest {
|
||||
val node = mockk<Node>(relaxed = true)
|
||||
ourNodeInfoFlow.value = node
|
||||
connectionStateFlow.value = ConnectionState.Connected
|
||||
every { radioPrefs.devAddr } returns "x123" // BLE
|
||||
|
||||
val hw = mockk<org.meshtastic.core.model.DeviceHardware> { every { requiresDfu } returns false }
|
||||
coEvery { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw)
|
||||
|
||||
useCase().test {
|
||||
assertFalse(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.RadioController
|
||||
|
||||
class MeshLocationUseCaseTest {
|
||||
|
||||
private lateinit var radioController: RadioController
|
||||
private lateinit var useCase: MeshLocationUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
radioController = mockk(relaxed = true)
|
||||
useCase = MeshLocationUseCase(radioController)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startProvidingLocation calls radioController`() {
|
||||
useCase.startProvidingLocation()
|
||||
verify { radioController.startProvideLocation() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stopProvidingLocation calls radioController`() {
|
||||
useCase.stopProvidingLocation()
|
||||
verify { radioController.stopProvideLocation() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Routing
|
||||
|
||||
class ProcessRadioResponseUseCaseTest {
|
||||
|
||||
private lateinit var useCase: ProcessRadioResponseUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
useCase = ProcessRadioResponseUseCase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with routing error returns error result`() {
|
||||
// Arrange
|
||||
val packet =
|
||||
MeshPacket(
|
||||
from = 123,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.ROUTING_APP,
|
||||
request_id = 42,
|
||||
payload = Routing(error_reason = Routing.Error.NO_ROUTE).encode().toByteString(),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = useCase(packet, 123, setOf(42))
|
||||
|
||||
// Assert
|
||||
assertTrue(result is RadioResponseResult.Error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with metadata response returns metadata result`() {
|
||||
// Arrange
|
||||
val metadata = DeviceMetadata(firmware_version = "2.5.0")
|
||||
val adminMsg = AdminMessage(get_device_metadata_response = metadata)
|
||||
val packet =
|
||||
MeshPacket(
|
||||
from = 123,
|
||||
decoded = Data(
|
||||
portnum = PortNum.ADMIN_APP,
|
||||
request_id = 42,
|
||||
payload = adminMsg.encode().toByteString(),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = useCase(packet, 123, setOf(42))
|
||||
|
||||
// Assert
|
||||
assertTrue(result is RadioResponseResult.Metadata)
|
||||
assertEquals("2.5.0", (result as RadioResponseResult.Metadata).metadata.firmware_version)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke with canned messages response returns canned messages result`() {
|
||||
// Arrange
|
||||
val adminMsg = AdminMessage(get_canned_message_module_messages_response = "Hello World")
|
||||
val packet =
|
||||
MeshPacket(
|
||||
from = 123,
|
||||
decoded = Data(
|
||||
portnum = PortNum.ADMIN_APP,
|
||||
request_id = 42,
|
||||
payload = adminMsg.encode().toByteString(),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = useCase(packet, 123, setOf(42))
|
||||
|
||||
// Assert
|
||||
assertTrue(result is RadioResponseResult.CannedMessages)
|
||||
assertEquals("Hello World", (result as RadioResponseResult.CannedMessages).messages)
|
||||
}
|
||||
|
||||
private fun ByteArray.toByteString() = okio.ByteString.of(*this)
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
class RadioConfigUseCaseTest {
|
||||
|
||||
private lateinit var radioController: RadioController
|
||||
private lateinit var useCase: RadioConfigUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
radioController = mockk(relaxed = true)
|
||||
useCase = RadioConfigUseCase(radioController)
|
||||
every { radioController.getPacketId() } returns 42
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setOwner calls radioController and returns packetId`() = runTest {
|
||||
val user = User(long_name = "New Name")
|
||||
val result = useCase.setOwner(123, user)
|
||||
|
||||
coVerify { radioController.setOwner(123, user, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getOwner calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getOwner(123)
|
||||
|
||||
coVerify { radioController.getOwner(123, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setConfig calls radioController and returns packetId`() = runTest {
|
||||
val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT))
|
||||
val result = useCase.setConfig(123, config)
|
||||
|
||||
coVerify { radioController.setConfig(123, config, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getConfig calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getConfig(123, 1)
|
||||
|
||||
coVerify { radioController.getConfig(123, 1, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setModuleConfig calls radioController and returns packetId`() = runTest {
|
||||
val config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true))
|
||||
val result = useCase.setModuleConfig(123, config)
|
||||
|
||||
coVerify { radioController.setModuleConfig(123, config, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getModuleConfig calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getModuleConfig(123, 2)
|
||||
|
||||
coVerify { radioController.getModuleConfig(123, 2, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getChannel calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getChannel(123, 0)
|
||||
|
||||
coVerify { radioController.getChannel(123, 0, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setRemoteChannel calls radioController and returns packetId`() = runTest {
|
||||
val channel = Channel(index = 0)
|
||||
val result = useCase.setRemoteChannel(123, channel)
|
||||
|
||||
coVerify { radioController.setRemoteChannel(123, channel, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setFixedPosition calls radioController`() = runTest {
|
||||
val pos = Position(1.0, 2.0, 3)
|
||||
useCase.setFixedPosition(123, pos)
|
||||
|
||||
coVerify { radioController.setFixedPosition(123, pos) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeFixedPosition calls radioController with zero position`() = runTest {
|
||||
useCase.removeFixedPosition(123)
|
||||
|
||||
coVerify { radioController.setFixedPosition(123, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setRingtone calls radioController`() = runTest {
|
||||
useCase.setRingtone(123, "ring")
|
||||
coVerify { radioController.setRingtone(123, "ring") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRingtone calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getRingtone(123)
|
||||
coVerify { radioController.getRingtone(123, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setCannedMessages calls radioController`() = runTest {
|
||||
useCase.setCannedMessages(123, "msg")
|
||||
coVerify { radioController.setCannedMessages(123, "msg") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCannedMessages calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getCannedMessages(123)
|
||||
coVerify { radioController.getCannedMessages(123, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDeviceConnectionStatus calls radioController and returns packetId`() = runTest {
|
||||
val result = useCase.getDeviceConnectionStatus(123)
|
||||
coVerify { radioController.getDeviceConnectionStatus(123, 42) }
|
||||
assertEquals(42, result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
|
||||
class SetAppIntroCompletedUseCaseTest {
|
||||
|
||||
private lateinit var uiPreferencesDataSource: UiPreferencesDataSource
|
||||
private lateinit var useCase: SetAppIntroCompletedUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
uiPreferencesDataSource = mockk(relaxed = true)
|
||||
useCase = SetAppIntroCompletedUseCase(uiPreferencesDataSource)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke calls setAppIntroCompleted on data source`() {
|
||||
// Act
|
||||
useCase(true)
|
||||
|
||||
// Assert
|
||||
verify { uiPreferencesDataSource.setAppIntroCompleted(true) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
|
||||
class SetDatabaseCacheLimitUseCaseTest {
|
||||
|
||||
private lateinit var databaseManager: DatabaseManager
|
||||
private lateinit var useCase: SetDatabaseCacheLimitUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
databaseManager = mockk(relaxed = true)
|
||||
useCase = SetDatabaseCacheLimitUseCase(databaseManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke calls setCacheLimit with clamped value`() {
|
||||
// Act & Assert
|
||||
useCase(0)
|
||||
verify { databaseManager.setCacheLimit(DatabaseConstants.MIN_CACHE_LIMIT) }
|
||||
|
||||
useCase(100)
|
||||
verify { databaseManager.setCacheLimit(DatabaseConstants.MAX_CACHE_LIMIT) }
|
||||
|
||||
useCase(5)
|
||||
verify { databaseManager.setCacheLimit(5) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
|
||||
class SetMeshLogSettingsUseCaseTest {
|
||||
|
||||
private lateinit var meshLogRepository: MeshLogRepository
|
||||
private lateinit var meshLogPrefs: MeshLogPrefs
|
||||
private lateinit var useCase: SetMeshLogSettingsUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
meshLogRepository = mockk(relaxed = true)
|
||||
meshLogPrefs = mockk(relaxed = true)
|
||||
useCase = SetMeshLogSettingsUseCase(meshLogRepository, meshLogPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setRetentionDays clamps and updates prefs and repository`() = runTest {
|
||||
// Act
|
||||
useCase.setRetentionDays(MeshLogPrefs.MIN_RETENTION_DAYS - 1)
|
||||
|
||||
// Assert
|
||||
verify { meshLogPrefs.retentionDays = MeshLogPrefs.MIN_RETENTION_DAYS }
|
||||
coVerify { meshLogRepository.deleteLogsOlderThan(MeshLogPrefs.MIN_RETENTION_DAYS) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLoggingEnabled true triggers cleanup`() = runTest {
|
||||
// Arrange
|
||||
every { meshLogPrefs.retentionDays } returns 30
|
||||
|
||||
// Act
|
||||
useCase.setLoggingEnabled(true)
|
||||
|
||||
// Assert
|
||||
verify { meshLogPrefs.loggingEnabled = true }
|
||||
coVerify { meshLogRepository.deleteLogsOlderThan(30) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setLoggingEnabled false triggers deletion`() = runTest {
|
||||
// Act
|
||||
useCase.setLoggingEnabled(false)
|
||||
|
||||
// Assert
|
||||
verify { meshLogPrefs.loggingEnabled = false }
|
||||
coVerify { meshLogRepository.deleteAll() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
|
||||
class SetProvideLocationUseCaseTest {
|
||||
|
||||
private lateinit var uiPrefs: UiPrefs
|
||||
private lateinit var useCase: SetProvideLocationUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
uiPrefs = mockk(relaxed = true)
|
||||
useCase = SetProvideLocationUseCase(uiPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke calls setShouldProvideNodeLocation on uiPrefs`() {
|
||||
// Act
|
||||
useCase(1234, true)
|
||||
|
||||
// Assert
|
||||
verify { uiPrefs.setShouldProvideNodeLocation(1234, true) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
|
||||
class SetThemeUseCaseTest {
|
||||
|
||||
private lateinit var uiPreferencesDataSource: UiPreferencesDataSource
|
||||
private lateinit var useCase: SetThemeUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
uiPreferencesDataSource = mockk(relaxed = true)
|
||||
useCase = SetThemeUseCase(uiPreferencesDataSource)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke calls setTheme on data source`() {
|
||||
// Act
|
||||
useCase(1)
|
||||
|
||||
// Assert
|
||||
verify { uiPreferencesDataSource.setTheme(1) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
|
||||
class ToggleAnalyticsUseCaseTest {
|
||||
|
||||
private lateinit var analyticsPrefs: AnalyticsPrefs
|
||||
private lateinit var useCase: ToggleAnalyticsUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
analyticsPrefs = mockk(relaxed = true)
|
||||
useCase = ToggleAnalyticsUseCase(analyticsPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles analytics from false to true`() {
|
||||
// Arrange
|
||||
every { analyticsPrefs.analyticsAllowed } returns false
|
||||
|
||||
// Act
|
||||
useCase()
|
||||
|
||||
// Assert
|
||||
verify { analyticsPrefs.analyticsAllowed = true }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles analytics from true to false`() {
|
||||
// Arrange
|
||||
every { analyticsPrefs.analyticsAllowed } returns true
|
||||
|
||||
// Act
|
||||
useCase()
|
||||
|
||||
// Assert
|
||||
verify { analyticsPrefs.analyticsAllowed = false }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
|
||||
class ToggleHomoglyphEncodingUseCaseTest {
|
||||
|
||||
private lateinit var homoglyphEncodingPrefs: HomoglyphPrefs
|
||||
private lateinit var useCase: ToggleHomoglyphEncodingUseCase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
homoglyphEncodingPrefs = mockk(relaxed = true)
|
||||
useCase = ToggleHomoglyphEncodingUseCase(homoglyphEncodingPrefs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles homoglyph encoding from false to true`() {
|
||||
// Arrange
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false
|
||||
|
||||
// Act
|
||||
useCase()
|
||||
|
||||
// Assert
|
||||
verify { homoglyphEncodingPrefs.homoglyphEncodingEnabled = true }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke toggles homoglyph encoding from true to false`() {
|
||||
// Arrange
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns true
|
||||
|
||||
// Act
|
||||
useCase()
|
||||
|
||||
// Assert
|
||||
verify { homoglyphEncodingPrefs.homoglyphEncodingEnabled = false }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* Copyright (c) 2026 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
|
||||
|
|
@ -14,8 +14,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.service
|
||||
package org.meshtastic.core.model
|
||||
|
||||
sealed class ConnectionState {
|
||||
/** We are disconnected from the device, and we should be trying to reconnect. */
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface RadioController {
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
val clientNotification: StateFlow<ClientNotification?>
|
||||
|
||||
suspend fun sendMessage(packet: DataPacket)
|
||||
|
||||
fun clearClientNotification()
|
||||
|
||||
// Abstracted ServiceActions
|
||||
suspend fun favoriteNode(nodeNum: Int)
|
||||
|
||||
suspend fun sendSharedContact(nodeNum: Int)
|
||||
|
||||
// Radio configuration
|
||||
suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int)
|
||||
|
||||
suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int)
|
||||
|
||||
suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int)
|
||||
|
||||
suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int)
|
||||
|
||||
suspend fun setFixedPosition(destNum: Int, position: Position)
|
||||
|
||||
suspend fun setRingtone(destNum: Int, ringtone: String)
|
||||
|
||||
suspend fun setCannedMessages(destNum: Int, messages: String)
|
||||
|
||||
// Admin get operations
|
||||
suspend fun getOwner(destNum: Int, packetId: Int)
|
||||
|
||||
suspend fun getConfig(destNum: Int, configType: Int, packetId: Int)
|
||||
|
||||
suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int)
|
||||
|
||||
suspend fun getChannel(destNum: Int, index: Int, packetId: Int)
|
||||
|
||||
suspend fun getRingtone(destNum: Int, packetId: Int)
|
||||
|
||||
suspend fun getCannedMessages(destNum: Int, packetId: Int)
|
||||
|
||||
suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int)
|
||||
|
||||
// Admin operations
|
||||
suspend fun reboot(destNum: Int, packetId: Int)
|
||||
|
||||
suspend fun shutdown(destNum: Int, packetId: Int)
|
||||
|
||||
suspend fun factoryReset(destNum: Int, packetId: Int)
|
||||
|
||||
suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean)
|
||||
|
||||
suspend fun removeByNodenum(packetId: Int, nodeNum: Int)
|
||||
|
||||
// Batch editing
|
||||
suspend fun beginEditSettings(destNum: Int)
|
||||
|
||||
suspend fun commitEditSettings(destNum: Int)
|
||||
|
||||
// Helpers
|
||||
fun getPacketId(): Int
|
||||
|
||||
/** Starts providing the phone's location to the mesh. */
|
||||
fun startProvideLocation()
|
||||
|
||||
/** Stops providing the phone's location to the mesh. */
|
||||
fun stopProvideLocation()
|
||||
}
|
||||
|
|
@ -16,7 +16,10 @@
|
|||
*/
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
|
||||
plugins { alias(libs.plugins.meshtastic.android.library) }
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.android.library)
|
||||
alias(libs.plugins.meshtastic.hilt)
|
||||
}
|
||||
|
||||
configure<LibraryExtension> {
|
||||
buildFeatures { aidl = true }
|
||||
|
|
@ -28,6 +31,7 @@ configure<LibraryExtension> {
|
|||
dependencies {
|
||||
api(projects.core.api)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.prefs)
|
||||
|
|
@ -39,4 +43,5 @@ dependencies {
|
|||
testImplementation(libs.junit)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.turbine)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.service
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Suppress("TooManyFunctions")
|
||||
class AndroidRadioControllerImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
) : RadioController {
|
||||
|
||||
override val connectionState: StateFlow<ConnectionState>
|
||||
get() = serviceRepository.connectionState
|
||||
|
||||
override val clientNotification: StateFlow<ClientNotification?>
|
||||
get() = serviceRepository.clientNotification
|
||||
|
||||
override suspend fun sendMessage(packet: DataPacket) {
|
||||
// Bridging to the existing flow via IMeshService
|
||||
serviceRepository.meshService?.send(packet)
|
||||
}
|
||||
|
||||
override fun clearClientNotification() {
|
||||
serviceRepository.clearClientNotification()
|
||||
}
|
||||
|
||||
override suspend fun favoriteNode(nodeNum: Int) {
|
||||
val nodeDef = nodeRepository.getNode(nodeNum.toString())
|
||||
serviceRepository.onServiceAction(ServiceAction.Favorite(nodeDef))
|
||||
}
|
||||
|
||||
override suspend fun sendSharedContact(nodeNum: Int) {
|
||||
val nodeDef = nodeRepository.getNode(nodeNum.toString())
|
||||
val contact =
|
||||
org.meshtastic.proto.SharedContact(
|
||||
node_num = nodeDef.num,
|
||||
user = nodeDef.user,
|
||||
manually_verified = nodeDef.manuallyVerified,
|
||||
)
|
||||
serviceRepository.onServiceAction(ServiceAction.SendContact(contact))
|
||||
}
|
||||
|
||||
override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) {
|
||||
serviceRepository.meshService?.setRemoteOwner(packetId, destNum, user.encode())
|
||||
}
|
||||
|
||||
override suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int) {
|
||||
serviceRepository.meshService?.setRemoteConfig(packetId, destNum, config.encode())
|
||||
}
|
||||
|
||||
override suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int) {
|
||||
serviceRepository.meshService?.setModuleConfig(packetId, destNum, config.encode())
|
||||
}
|
||||
|
||||
override suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int) {
|
||||
serviceRepository.meshService?.setRemoteChannel(packetId, destNum, channel.encode())
|
||||
}
|
||||
|
||||
override suspend fun setFixedPosition(destNum: Int, position: org.meshtastic.core.model.Position) {
|
||||
serviceRepository.meshService?.setFixedPosition(destNum, position)
|
||||
}
|
||||
|
||||
override suspend fun setRingtone(destNum: Int, ringtone: String) {
|
||||
serviceRepository.meshService?.setRingtone(destNum, ringtone)
|
||||
}
|
||||
|
||||
override suspend fun setCannedMessages(destNum: Int, messages: String) {
|
||||
serviceRepository.meshService?.setCannedMessages(destNum, messages)
|
||||
}
|
||||
|
||||
override suspend fun getOwner(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getRemoteOwner(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getRemoteConfig(packetId, destNum, configType)
|
||||
}
|
||||
|
||||
override suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getModuleConfig(packetId, destNum, moduleConfigType)
|
||||
}
|
||||
|
||||
override suspend fun getChannel(destNum: Int, index: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getRemoteChannel(packetId, destNum, index)
|
||||
}
|
||||
|
||||
override suspend fun getRingtone(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getRingtone(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun getCannedMessages(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getCannedMessages(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.getDeviceConnectionStatus(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun reboot(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.requestReboot(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun shutdown(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.requestShutdown(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun factoryReset(destNum: Int, packetId: Int) {
|
||||
serviceRepository.meshService?.requestFactoryReset(packetId, destNum)
|
||||
}
|
||||
|
||||
override suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean) {
|
||||
serviceRepository.meshService?.requestNodedbReset(packetId, destNum, preserveFavorites)
|
||||
}
|
||||
|
||||
override suspend fun removeByNodenum(packetId: Int, nodeNum: Int) {
|
||||
serviceRepository.meshService?.removeByNodenum(packetId, nodeNum)
|
||||
}
|
||||
|
||||
override suspend fun beginEditSettings(destNum: Int) {
|
||||
serviceRepository.meshService?.beginEditSettings(destNum)
|
||||
}
|
||||
|
||||
override suspend fun commitEditSettings(destNum: Int) {
|
||||
serviceRepository.meshService?.commitEditSettings(destNum)
|
||||
}
|
||||
|
||||
override fun getPacketId(): Int = serviceRepository.meshService?.getPacketId() ?: 0
|
||||
|
||||
override fun startProvideLocation() {
|
||||
serviceRepository.meshService?.startProvideLocation()
|
||||
}
|
||||
|
||||
override fun stopProvideLocation() {
|
||||
serviceRepository.meshService?.stopProvideLocation()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import javax.inject.Inject
|
||||
|
|
@ -44,7 +45,7 @@ data class TracerouteResponse(
|
|||
/** Repository class for managing the [IMeshService] instance and connection state */
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
class ServiceRepository @Inject constructor() {
|
||||
open class ServiceRepository @Inject constructor() {
|
||||
var meshService: IMeshService? = null
|
||||
private set
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ class ServiceRepository @Inject constructor() {
|
|||
|
||||
// Connection state to our radio device
|
||||
private val _connectionState: MutableStateFlow<ConnectionState> = MutableStateFlow(ConnectionState.Disconnected)
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
open val connectionState: StateFlow<ConnectionState>
|
||||
get() = _connectionState
|
||||
|
||||
fun setConnectionState(connectionState: ConnectionState) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.core.service.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.service.AndroidRadioControllerImpl
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class ServiceModule {
|
||||
|
||||
@Binds abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import org.meshtastic.core.database.entity.FirmwareRelease
|
|||
import org.meshtastic.core.database.entity.FirmwareReleaseType
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.datastore.BootloaderWarningDataSource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
import org.meshtastic.core.prefs.radio.isBle
|
||||
|
|
@ -71,7 +72,6 @@ import org.meshtastic.core.resources.firmware_update_unknown_hardware
|
|||
import org.meshtastic.core.resources.firmware_update_updating
|
||||
import org.meshtastic.core.resources.firmware_update_validating
|
||||
import org.meshtastic.core.resources.unknown
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import com.google.accompanist.permissions.rememberPermissionState
|
|||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun AppIntroductionScreen(onDone: () -> Unit, @Suppress("unused") viewModel: IntroViewModel = hiltViewModel()) {
|
||||
fun AppIntroductionScreen(onDone: () -> Unit, viewModel: IntroViewModel = hiltViewModel()) {
|
||||
val notificationPermissionState: PermissionState? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ import org.meshtastic.core.data.repository.NodeRepository
|
|||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ configure<LibraryExtension> { namespace = "org.meshtastic.feature.messaging" }
|
|||
|
||||
dependencies {
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.prefs)
|
||||
|
|
@ -50,6 +52,9 @@ dependencies {
|
|||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.kermit)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.hilt.work)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
|
||||
|
|
@ -59,4 +64,8 @@ dependencies {
|
|||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.turbine)
|
||||
testImplementation(libs.androidx.work.testing)
|
||||
testImplementation(libs.androidx.test.core)
|
||||
testImplementation(libs.robolectric)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.pluralStringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
|
|||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.domain.usecase.SendMessageUseCase
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs
|
||||
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
|
||||
|
|
@ -47,7 +48,6 @@ import org.meshtastic.core.service.MeshServiceNotifications
|
|||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.feature.messaging.domain.usecase.SendMessageUseCase
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.core.domain.MessageQueue
|
||||
import org.meshtastic.feature.messaging.domain.worker.WorkManagerMessageQueue
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class MessagingModule {
|
||||
|
||||
@Binds abstract fun bindMessageQueue(impl: WorkManagerMessageQueue): MessageQueue
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.RadioController
|
||||
|
||||
@HiltWorker
|
||||
class SendMessageWorker
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val radioController: RadioController,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException", "ReturnCount")
|
||||
override suspend fun doWork(): Result {
|
||||
val packetId = inputData.getInt(KEY_PACKET_ID, 0)
|
||||
if (packetId == 0) return Result.failure()
|
||||
|
||||
// Verify we are connected before attempting to send to avoid unnecessary Exception bubbling
|
||||
if (radioController.connectionState.value != ConnectionState.Connected) {
|
||||
return Result.retry()
|
||||
}
|
||||
|
||||
val packetEntity =
|
||||
packetRepository.getPacketByPacketId(packetId)
|
||||
?: return Result.failure() // Packet no longer exists in DB? Do not retry.
|
||||
|
||||
val packetData = packetEntity.packet.data
|
||||
|
||||
return try {
|
||||
radioController.sendMessage(packetData)
|
||||
packetRepository.updateMessageStatus(packetData, MessageStatus.ENROUTE)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
packetRepository.updateMessageStatus(packetData, MessageStatus.ERROR)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_PACKET_ID = "packet_id"
|
||||
const val WORK_NAME_PREFIX = "send_message_"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.worker
|
||||
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import org.meshtastic.core.domain.MessageQueue
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Android implementation of [MessageQueue] that uses [WorkManager] for reliable background transmission. */
|
||||
@Singleton
|
||||
class WorkManagerMessageQueue @Inject constructor(private val workManager: WorkManager) : MessageQueue {
|
||||
|
||||
override suspend fun enqueue(packetId: Int) {
|
||||
val workRequest =
|
||||
OneTimeWorkRequestBuilder<SendMessageWorker>()
|
||||
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId))
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
"${SendMessageWorker.WORK_NAME_PREFIX}$packetId",
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
workRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package org.meshtastic.feature.messaging
|
|||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.common.util.HomoglyphCharacterStringTransformer
|
||||
|
||||
class HomoglyphCharacterTransformTest {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright (c) 2026 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/>.
|
||||
*/
|
||||
package org.meshtastic.feature.messaging.domain.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.testing.TestListenableWorkerBuilder
|
||||
import androidx.work.workDataOf
|
||||
import io.mockk.Runs
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.PacketEntity
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class SendMessageWorkerTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var packetRepository: PacketRepository
|
||||
private lateinit var radioController: RadioController
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
packetRepository = mockk(relaxed = true)
|
||||
radioController = mockk(relaxed = true)
|
||||
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `doWork returns success when packet is sent successfully`() = runTest {
|
||||
// Arrange
|
||||
val packetId = 12345
|
||||
val dataPacket = DataPacket("dest", 0, "Hello")
|
||||
val packet = mockk<Packet>(relaxed = true)
|
||||
val packetEntity = PacketEntity(packet = packet)
|
||||
every { packet.data } returns dataPacket
|
||||
coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity
|
||||
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||
coEvery { radioController.sendMessage(any()) } just Runs
|
||||
coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs
|
||||
|
||||
val worker =
|
||||
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
||||
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId))
|
||||
.setWorkerFactory(
|
||||
object : androidx.work.WorkerFactory() {
|
||||
override fun createWorker(
|
||||
appContext: Context,
|
||||
workerClassName: String,
|
||||
workerParameters: WorkerParameters,
|
||||
): ListenableWorker? =
|
||||
SendMessageWorker(appContext, workerParameters, packetRepository, radioController)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
|
||||
// Act
|
||||
val result = worker.doWork()
|
||||
|
||||
// Assert
|
||||
assertEquals(ListenableWorker.Result.success(), result)
|
||||
coVerify { radioController.sendMessage(dataPacket) }
|
||||
coVerify { packetRepository.updateMessageStatus(dataPacket, MessageStatus.ENROUTE) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `doWork returns retry when radio is disconnected`() = runTest {
|
||||
// Arrange
|
||||
val packetId = 12345
|
||||
val dataPacket = DataPacket("dest", 0, "Hello")
|
||||
val packet = mockk<Packet>(relaxed = true)
|
||||
val packetEntity = PacketEntity(packet = packet)
|
||||
every { packet.data } returns dataPacket
|
||||
coEvery { packetRepository.getPacketByPacketId(packetId) } returns packetEntity
|
||||
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
|
||||
|
||||
val worker =
|
||||
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
||||
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId))
|
||||
.setWorkerFactory(
|
||||
object : androidx.work.WorkerFactory() {
|
||||
override fun createWorker(
|
||||
appContext: Context,
|
||||
workerClassName: String,
|
||||
workerParameters: WorkerParameters,
|
||||
): ListenableWorker? =
|
||||
SendMessageWorker(appContext, workerParameters, packetRepository, radioController)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
|
||||
// Act
|
||||
val result = worker.doWork()
|
||||
|
||||
// Assert
|
||||
assertEquals(ListenableWorker.Result.retry(), result)
|
||||
coVerify(exactly = 0) { radioController.sendMessage(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `doWork returns failure when packet is missing`() = runTest {
|
||||
// Arrange
|
||||
val packetId = 999
|
||||
coEvery { packetRepository.getPacketByPacketId(packetId) } returns null
|
||||
|
||||
val worker =
|
||||
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
||||
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId))
|
||||
.setWorkerFactory(
|
||||
object : androidx.work.WorkerFactory() {
|
||||
override fun createWorker(
|
||||
appContext: Context,
|
||||
workerClassName: String,
|
||||
workerParameters: WorkerParameters,
|
||||
): ListenableWorker? =
|
||||
SendMessageWorker(appContext, workerParameters, packetRepository, radioController)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
|
||||
// Act
|
||||
val result = worker.doWork()
|
||||
|
||||
// Assert
|
||||
assertEquals(ListenableWorker.Result.failure(), result)
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -63,7 +64,6 @@ import org.meshtastic.core.resources.elevation_suffix
|
|||
import org.meshtastic.core.resources.signal_quality
|
||||
import org.meshtastic.core.resources.unknown_username
|
||||
import org.meshtastic.core.resources.voltage
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.AirQualityInfo
|
||||
import org.meshtastic.core.ui.component.ChannelInfo
|
||||
import org.meshtastic.core.ui.component.DistanceInfo
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.connected
|
||||
import org.meshtastic.core.resources.connecting
|
||||
|
|
@ -46,7 +47,6 @@ import org.meshtastic.core.resources.favorite
|
|||
import org.meshtastic.core.resources.mute_always
|
||||
import org.meshtastic.core.resources.unmessageable
|
||||
import org.meshtastic.core.resources.unmonitored_or_infrastructure
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.icon.CloudDone
|
||||
import org.meshtastic.core.ui.icon.CloudOffTwoTone
|
||||
import org.meshtastic.core.ui.icon.CloudSync
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.add_favorite
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
|
|
@ -79,7 +80,6 @@ import org.meshtastic.core.resources.remove
|
|||
import org.meshtastic.core.resources.remove_favorite
|
||||
import org.meshtastic.core.resources.remove_ignored
|
||||
import org.meshtastic.core.resources.unmute
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticImportFAB
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
implementation(projects.core.data)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.datastore)
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.nfc)
|
||||
|
|
@ -57,7 +58,10 @@ dependencies {
|
|||
implementation(libs.nordic.common.permissions.ble)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockk)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.turbine)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
testImplementation(libs.androidx.test.ext.junit)
|
||||
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ import org.meshtastic.core.resources.preserve_favorites
|
|||
import org.meshtastic.core.resources.remotely_administrating
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.feature.settings.component.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.radio.AdminRoute
|
||||
import org.meshtastic.feature.settings.radio.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigState
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.ResponseState
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ import org.meshtastic.core.resources.device_configuration
|
|||
import org.meshtastic.core.resources.remotely_administrating
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.feature.settings.component.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.radio.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ import org.meshtastic.core.resources.module_settings
|
|||
import org.meshtastic.core.resources.remotely_administrating
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.feature.settings.component.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.navigation.ModuleRoute
|
||||
import org.meshtastic.feature.settings.radio.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue