feat: Implement iOS support and unify Compose Multiplatform infrastructure (#4876)

This commit is contained in:
James Rich 2026-03-21 18:19:13 -05:00 committed by GitHub
parent f04924ded5
commit d136b162a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 2208 additions and 2432 deletions

View file

@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
@ -27,6 +26,7 @@ import kotlinx.coroutines.flow.onEach
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
@ -58,7 +58,7 @@ class CommandSenderImpl(
private val nodeManager: NodeManager,
private val radioConfigRepository: RadioConfigRepository,
) : CommandSender {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private val currentPacketId = atomic(Random(nowMillis).nextLong().absoluteValue)
private val sessionPasskey = atomic(ByteString.EMPTY)
override val tracerouteStartTimes = mutableMapOf<Int, Long>()

View file

@ -17,13 +17,13 @@
package org.meshtastic.core.data.manager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okio.ByteString.Companion.toByteString
import org.koin.core.annotation.Single
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ignoreException
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MeshUser
@ -64,7 +64,7 @@ class MeshActionHandlerImpl(
private val notificationManager: NotificationManager,
private val messageProcessor: Lazy<MeshMessageProcessor>,
) : MeshActionHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
override fun start(scope: CoroutineScope) {
this.scope = scope

View file

@ -18,12 +18,12 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import okio.IOException
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.MeshConfigFlowManager
@ -56,7 +56,7 @@ class MeshConfigFlowManagerImpl(
private val commandSender: CommandSender,
private val packetHandler: PacketHandler,
) : MeshConfigFlowManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private val configOnlyNonce = 69420
private val nodeInfoNonce = 69421
private val wantConfigDelay = 100L

View file

@ -17,7 +17,6 @@
package org.meshtastic.core.data.manager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.RadioConfigRepository
@ -41,7 +41,7 @@ class MeshConfigHandlerImpl(
private val serviceRepository: ServiceRepository,
private val nodeManager: NodeManager,
) : MeshConfigHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private val _localConfig = MutableStateFlow(LocalConfig())
override val localConfig = _localConfig.asStateFlow()

View file

@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.ConnectionState
@ -89,7 +89,7 @@ class MeshConnectionManagerImpl(
private val workerManager: MeshWorkerManager,
private val appWidgetUpdater: AppWidgetUpdater,
) : MeshConnectionManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private var sleepTimeout: Job? = null
private var locationRequestsJob: Job? = null
private var handshakeTimeout: Job? = null

View file

@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
@ -30,6 +29,7 @@ import okio.ByteString.Companion.toByteString
import okio.IOException
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.DataPacket
@ -63,7 +63,7 @@ import org.meshtastic.core.repository.TracerouteHandler
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.critical_alert
import org.meshtastic.core.resources.error_duty_cycle
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.resources.low_battery_message
import org.meshtastic.core.resources.low_battery_title
import org.meshtastic.core.resources.unknown_username
@ -114,7 +114,7 @@ class MeshDataHandlerImpl(
private val radioConfigRepository: RadioConfigRepository,
private val messageFilter: MessageFilter,
) : MeshDataHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
override fun start(scope: CoroutineScope) {
this.scope = scope
@ -433,9 +433,13 @@ class MeshDataHandlerImpl(
if (shouldBatteryNotificationShow(fromNum, t, myNodeNum)) {
notificationManager.dispatch(
Notification(
title = getString(Res.string.low_battery_title, nextNode.user.short_name),
title =
getStringSuspend(
Res.string.low_battery_title,
nextNode.user.short_name,
),
message =
getString(
getStringSuspend(
Res.string.low_battery_message,
nextNode.user.long_name,
nextNode.deviceMetrics.battery_level ?: 0,
@ -502,7 +506,9 @@ class MeshDataHandlerImpl(
val payload = packet.decoded?.payload ?: return
val r = Routing.ADAPTER.decodeOrNull(payload, Logger) ?: return
if (r.error_reason == Routing.Error.DUTY_CYCLE_LIMIT) {
serviceRepository.setErrorMessage(getString(Res.string.error_duty_cycle), Severity.Warn)
scope.launch {
serviceRepository.setErrorMessage(getStringSuspend(Res.string.error_duty_cycle), Severity.Warn)
}
}
handleAckNak(
packet.decoded?.request_id ?: 0,
@ -659,25 +665,27 @@ class MeshDataHandlerImpl(
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
val isSilent = conversationMuted || nodeMuted
if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
notificationManager.dispatch(
Notification(
title = getSenderName(dataPacket),
message = dataPacket.alert ?: getString(Res.string.critical_alert),
category = Notification.Category.Alert,
contactKey = contactKey,
),
)
scope.launch {
notificationManager.dispatch(
Notification(
title = getSenderName(dataPacket),
message = dataPacket.alert ?: getStringSuspend(Res.string.critical_alert),
category = Notification.Category.Alert,
contactKey = contactKey,
),
)
}
} else if (updateNotification && !isSilent) {
scope.handledLaunch { updateNotification(contactKey, dataPacket, isSilent) }
}
}
private fun getSenderName(packet: DataPacket): String {
private suspend fun getSenderName(packet: DataPacket): String {
if (packet.from == DataPacket.ID_LOCAL) {
val myId = nodeManager.getMyId()
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getString(Res.string.unknown_username)
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
}
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getString(Res.string.unknown_username)
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
}
private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket, isSilent: Boolean) {
@ -701,7 +709,7 @@ class MeshDataHandlerImpl(
}
PortNum.WAYPOINT_APP.value -> {
val message = getString(Res.string.waypoint_received, dataPacket.waypoint!!.name)
val message = getStringSuspend(Res.string.waypoint_received, dataPacket.waypoint!!.name)
notificationManager.dispatch(
Notification(
title = getSenderName(dataPacket),

View file

@ -18,7 +18,6 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
@ -28,6 +27,7 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.MeshLog
@ -55,7 +55,7 @@ class MeshMessageProcessorImpl(
private val router: Lazy<MeshRouter>,
private val fromRadioDispatcher: FromRadioPacketHandler,
) : MeshMessageProcessor {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private val mapsMutex = Mutex()
private val logUuidByPacketId = mutableMapOf<Int, String>()

View file

@ -19,13 +19,13 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.network.repository.MQTTRepository
import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.PacketHandler
@ -39,7 +39,7 @@ class MqttManagerImpl(
private val packetHandler: PacketHandler,
private val serviceRepository: ServiceRepository,
) : MqttManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private var mqttMessageFlow: Job? = null
override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) {

View file

@ -18,10 +18,10 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.NeighborInfoHandler
@ -38,7 +38,7 @@ class NeighborInfoHandlerImpl(
private val commandSender: CommandSender,
private val serviceBroadcasts: ServiceBroadcasts,
) : NeighborInfoHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
override fun start(scope: CoroutineScope) {
this.scope = scope

View file

@ -21,13 +21,13 @@ import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.update
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import okio.ByteString
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceMetrics
import org.meshtastic.core.model.EnvironmentMetrics
@ -43,7 +43,7 @@ import org.meshtastic.core.repository.Notification
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.resources.new_node_seen
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.HardwareModel
@ -62,7 +62,7 @@ class NodeManagerImpl(
private val serviceBroadcasts: ServiceBroadcasts,
private val notificationManager: NotificationManager,
) : NodeManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
private val _nodeDBbyNodeNum = atomic(persistentMapOf<Int, Node>())
private val _nodeDBbyID = atomic(persistentMapOf<String, Node>())
@ -196,13 +196,15 @@ class NodeManagerImpl(
node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified)
}
if (newNode && !shouldPreserve) {
notificationManager.dispatch(
Notification(
title = getString(Res.string.new_node_seen, next.user.short_name),
message = next.user.long_name,
category = Notification.Category.NodeEvent,
),
)
scope.handledLaunch {
notificationManager.dispatch(
Notification(
title = getStringSuspend(Res.string.new_node_seen, next.user.short_name),
message = next.user.long_name,
category = Notification.Category.NodeEvent,
),
)
}
}
next
}

View file

@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
@ -30,6 +29,7 @@ import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
@ -67,7 +67,7 @@ class PacketHandlerImpl(
}
private var queueJob: Job? = null
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private var scope: CoroutineScope = CoroutineScope(ioDispatcher)
private val queueMutex = Mutex()
private val queuedPackets = mutableListOf<MeshPacket>()

View file

@ -18,11 +18,11 @@ package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.fullRouteDiscovery
@ -44,7 +44,7 @@ class TracerouteHandlerImpl(
private val nodeRepository: NodeRepository,
private val commandSender: CommandSender,
) : TracerouteHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var scope: CoroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
override fun start(scope: CoroutineScope) {
this.scope = scope