2024-11-26 08:38:12 -03:00
|
|
|
/*
|
2026-02-03 18:01:12 -06:00
|
|
|
* Copyright (c) 2025-2026 Meshtastic LLC
|
2024-11-26 08:38:12 -03:00
|
|
|
*
|
|
|
|
|
* 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/>.
|
|
|
|
|
*/
|
2020-02-17 13:34:52 -08:00
|
|
|
package com.geeksville.mesh.model
|
|
|
|
|
|
2020-03-17 11:35:19 -07:00
|
|
|
import android.net.Uri
|
2025-10-07 15:35:44 -04:00
|
|
|
import androidx.compose.runtime.Composable
|
2022-02-08 13:50:21 -08:00
|
|
|
import androidx.lifecycle.ViewModel
|
2020-09-23 22:47:45 -04:00
|
|
|
import androidx.lifecycle.viewModelScope
|
2025-10-06 14:30:18 -04:00
|
|
|
import androidx.navigation.NavHostController
|
2025-12-28 08:30:15 -06:00
|
|
|
import co.touchlab.kermit.Logger
|
2022-02-08 13:50:21 -08:00
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
2025-12-10 09:09:47 -06:00
|
|
|
import kotlinx.coroutines.channels.BufferOverflow
|
2025-11-12 14:22:21 -08:00
|
|
|
import kotlinx.coroutines.flow.Flow
|
2025-12-10 09:09:47 -06:00
|
|
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
2022-02-08 13:50:21 -08:00
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
2025-08-07 14:17:01 -05:00
|
|
|
import kotlinx.coroutines.flow.SharedFlow
|
2024-11-19 11:59:28 -03:00
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
2022-02-08 13:50:21 -08:00
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
2025-12-10 09:09:47 -06:00
|
|
|
import kotlinx.coroutines.flow.asSharedFlow
|
2025-05-15 08:05:30 -05:00
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
2023-10-12 22:52:54 -03:00
|
|
|
import kotlinx.coroutines.flow.filterNotNull
|
2022-11-08 23:11:18 -03:00
|
|
|
import kotlinx.coroutines.flow.launchIn
|
2025-05-26 19:36:32 -05:00
|
|
|
import kotlinx.coroutines.flow.map
|
2025-06-03 19:58:21 -05:00
|
|
|
import kotlinx.coroutines.flow.mapNotNull
|
2022-11-08 23:11:18 -03:00
|
|
|
import kotlinx.coroutines.flow.onEach
|
2025-08-07 14:17:01 -05:00
|
|
|
import kotlinx.coroutines.flow.shareIn
|
2026-02-08 16:45:52 -06:00
|
|
|
import org.jetbrains.compose.resources.StringResource
|
2025-11-10 19:58:38 -05:00
|
|
|
import org.jetbrains.compose.resources.getString
|
2025-10-06 14:30:18 -04:00
|
|
|
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
2025-09-26 17:45:11 -04:00
|
|
|
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
|
|
|
|
import org.meshtastic.core.data.repository.MeshLogRepository
|
2025-09-24 16:23:05 -04:00
|
|
|
import org.meshtastic.core.database.entity.asDeviceVersion
|
2025-09-23 15:52:09 -04:00
|
|
|
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
2026-03-03 07:15:28 -06:00
|
|
|
import org.meshtastic.core.model.MeshActivity
|
|
|
|
|
import org.meshtastic.core.model.MyNodeInfo
|
|
|
|
|
import org.meshtastic.core.model.RadioController
|
2025-12-16 16:53:28 +00:00
|
|
|
import org.meshtastic.core.model.TracerouteMapAvailability
|
|
|
|
|
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
|
2026-03-03 07:15:28 -06:00
|
|
|
import org.meshtastic.core.model.service.TracerouteResponse
|
2026-02-08 16:45:52 -06:00
|
|
|
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
2026-03-03 07:15:28 -06:00
|
|
|
import org.meshtastic.core.repository.MeshServiceNotifications
|
|
|
|
|
import org.meshtastic.core.repository.NodeRepository
|
|
|
|
|
import org.meshtastic.core.repository.PacketRepository
|
|
|
|
|
import org.meshtastic.core.repository.RadioInterfaceService
|
2026-02-22 21:39:50 -06:00
|
|
|
import org.meshtastic.core.resources.Res
|
|
|
|
|
import org.meshtastic.core.resources.client_notification
|
|
|
|
|
import org.meshtastic.core.resources.compromised_keys
|
2026-03-03 07:15:28 -06:00
|
|
|
import org.meshtastic.core.service.AndroidServiceRepository
|
2025-09-30 16:55:56 -04:00
|
|
|
import org.meshtastic.core.service.IMeshService
|
2025-11-12 14:22:21 -08:00
|
|
|
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
2026-02-08 16:45:52 -06:00
|
|
|
import org.meshtastic.core.ui.util.AlertManager
|
|
|
|
|
import org.meshtastic.core.ui.util.ComposableContent
|
2025-10-13 16:04:29 -04:00
|
|
|
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
2026-02-03 18:01:12 -06:00
|
|
|
import org.meshtastic.proto.ChannelSet
|
|
|
|
|
import org.meshtastic.proto.ClientNotification
|
|
|
|
|
import org.meshtastic.proto.SharedContact
|
2022-02-08 13:50:21 -08:00
|
|
|
import javax.inject.Inject
|
2020-04-08 09:53:04 -07:00
|
|
|
|
2022-02-08 13:50:21 -08:00
|
|
|
@HiltViewModel
|
2026-02-20 06:41:52 -06:00
|
|
|
@Suppress("LongParameterList", "TooManyFunctions")
|
2025-07-25 07:07:01 -05:00
|
|
|
class UIViewModel
|
|
|
|
|
@Inject
|
|
|
|
|
constructor(
|
2024-11-26 09:57:23 -03:00
|
|
|
private val nodeDB: NodeRepository,
|
2026-03-03 07:15:28 -06:00
|
|
|
private val serviceRepository: AndroidServiceRepository,
|
|
|
|
|
private val radioController: RadioController,
|
2025-08-13 12:51:19 -05:00
|
|
|
radioInterfaceService: RadioInterfaceService,
|
2025-09-26 19:54:31 -04:00
|
|
|
meshLogRepository: MeshLogRepository,
|
2025-06-03 19:58:21 -05:00
|
|
|
firmwareReleaseRepository: FirmwareReleaseRepository,
|
2025-09-23 15:52:09 -04:00
|
|
|
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
2025-07-25 07:07:01 -05:00
|
|
|
private val meshServiceNotifications: MeshServiceNotifications,
|
2025-10-06 14:30:18 -04:00
|
|
|
private val analytics: PlatformAnalytics,
|
2025-10-12 08:22:46 -04:00
|
|
|
packetRepository: PacketRepository,
|
2026-02-08 16:45:52 -06:00
|
|
|
private val alertManager: AlertManager,
|
2025-09-30 18:22:22 -05:00
|
|
|
) : ViewModel() {
|
2020-09-23 22:47:45 -04:00
|
|
|
|
2025-09-23 15:52:09 -04:00
|
|
|
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
|
2025-05-17 11:39:53 -05:00
|
|
|
|
2026-02-03 18:01:12 -06:00
|
|
|
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmware_edition }
|
2025-07-25 07:07:01 -05:00
|
|
|
|
2026-02-03 18:01:12 -06:00
|
|
|
val clientNotification: StateFlow<ClientNotification?> = serviceRepository.clientNotification
|
2025-06-20 23:19:51 +00:00
|
|
|
|
2026-02-03 18:01:12 -06:00
|
|
|
fun clearClientNotification(notification: ClientNotification) {
|
2025-09-25 09:57:26 -04:00
|
|
|
serviceRepository.clearClientNotification()
|
2025-06-16 17:06:23 +00:00
|
|
|
meshServiceNotifications.clearClientNotification(notification)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 14:17:01 -05:00
|
|
|
/**
|
|
|
|
|
* Emits events for mesh network send/receive activity. This is a SharedFlow to ensure all events are delivered,
|
|
|
|
|
* even if they are the same.
|
|
|
|
|
*/
|
|
|
|
|
val meshActivity: SharedFlow<MeshActivity> =
|
|
|
|
|
radioInterfaceService.meshActivity.shareIn(viewModelScope, SharingStarted.Eagerly, 0)
|
|
|
|
|
|
2025-12-10 09:09:47 -06:00
|
|
|
private val _scrollToTopEventFlow =
|
|
|
|
|
MutableSharedFlow<ScrollToTopEvent>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
|
|
|
val scrollToTopEventFlow: Flow<ScrollToTopEvent> = _scrollToTopEventFlow.asSharedFlow()
|
2025-11-12 14:22:21 -08:00
|
|
|
|
|
|
|
|
fun emitScrollToTopEvent(event: ScrollToTopEvent) {
|
2025-12-10 09:09:47 -06:00
|
|
|
_scrollToTopEventFlow.tryEmit(event)
|
2025-11-12 14:22:21 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 16:45:52 -06:00
|
|
|
val currentAlert = alertManager.currentAlert
|
2025-05-17 11:39:53 -05:00
|
|
|
|
2025-12-16 16:53:28 +00:00
|
|
|
fun tracerouteMapAvailability(forwardRoute: List<Int>, returnRoute: List<Int>): TracerouteMapAvailability =
|
|
|
|
|
evaluateTracerouteMapAvailability(
|
|
|
|
|
forwardRoute = forwardRoute,
|
|
|
|
|
returnRoute = returnRoute,
|
|
|
|
|
positionedNodeNums =
|
|
|
|
|
nodeDB.nodeDBbyNum.value.values.filter { it.validPosition != null }.map { it.num }.toSet(),
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-17 11:39:53 -05:00
|
|
|
fun showAlert(
|
2026-02-08 16:45:52 -06:00
|
|
|
title: String? = null,
|
|
|
|
|
titleRes: StringResource? = null,
|
2025-05-17 11:39:53 -05:00
|
|
|
message: String? = null,
|
2026-02-08 16:45:52 -06:00
|
|
|
messageRes: StringResource? = null,
|
|
|
|
|
composableMessage: ComposableContent? = null,
|
2025-05-17 11:39:53 -05:00
|
|
|
html: String? = null,
|
|
|
|
|
onConfirm: (() -> Unit)? = {},
|
2026-02-08 16:45:52 -06:00
|
|
|
onDismiss: (() -> Unit)? = null,
|
|
|
|
|
confirmText: String? = null,
|
|
|
|
|
confirmTextRes: StringResource? = null,
|
|
|
|
|
dismissText: String? = null,
|
|
|
|
|
dismissTextRes: StringResource? = null,
|
2025-06-16 17:06:23 +00:00
|
|
|
choices: Map<String, () -> Unit> = emptyMap(),
|
2025-05-17 11:39:53 -05:00
|
|
|
) {
|
2026-02-08 16:45:52 -06:00
|
|
|
alertManager.showAlert(
|
|
|
|
|
title = title,
|
|
|
|
|
titleRes = titleRes,
|
|
|
|
|
message = message,
|
|
|
|
|
messageRes = messageRes,
|
|
|
|
|
composableMessage = composableMessage,
|
|
|
|
|
html = html,
|
|
|
|
|
onConfirm = onConfirm,
|
|
|
|
|
onDismiss = onDismiss,
|
|
|
|
|
confirmText = confirmText,
|
|
|
|
|
confirmTextRes = confirmTextRes,
|
|
|
|
|
dismissText = dismissText,
|
|
|
|
|
dismissTextRes = dismissTextRes,
|
|
|
|
|
choices = choices,
|
|
|
|
|
)
|
2025-05-17 11:39:53 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 16:45:52 -06:00
|
|
|
fun dismissAlert() {
|
|
|
|
|
alertManager.dismissAlert()
|
2025-05-17 11:39:53 -05:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val meshService: IMeshService?
|
2025-09-25 09:57:26 -04:00
|
|
|
get() = serviceRepository.meshService
|
2023-02-02 17:13:44 -03:00
|
|
|
|
2026-03-03 07:15:28 -06:00
|
|
|
fun setDeviceAddress(address: String) {
|
|
|
|
|
radioController.setDeviceAddress(address)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 08:22:46 -04:00
|
|
|
val unreadMessageCount =
|
2025-10-13 16:04:29 -04:00
|
|
|
packetRepository.getUnreadCountTotal().map { it.coerceAtLeast(0) }.stateInWhileSubscribed(initialValue = 0)
|
2025-10-12 08:22:46 -04:00
|
|
|
|
2026-02-14 10:07:03 -06:00
|
|
|
private val _navigationDeepLink = MutableSharedFlow<Uri>(replay = 1)
|
|
|
|
|
val navigationDeepLink = _navigationDeepLink.asSharedFlow()
|
|
|
|
|
|
|
|
|
|
fun handleNavigationDeepLink(uri: Uri) {
|
|
|
|
|
_navigationDeepLink.tryEmit(uri)
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 18:31:13 -03:00
|
|
|
// hardware info about our local device (can be null)
|
2026-03-03 07:15:28 -06:00
|
|
|
val myNodeInfo: StateFlow<MyNodeInfo?>
|
2025-07-25 07:07:01 -05:00
|
|
|
get() = nodeDB.myNodeInfo
|
|
|
|
|
|
2020-04-07 17:42:31 -07:00
|
|
|
init {
|
2025-09-25 09:57:26 -04:00
|
|
|
serviceRepository.errorMessage
|
2025-07-25 07:07:01 -05:00
|
|
|
.filterNotNull()
|
|
|
|
|
.onEach {
|
|
|
|
|
showAlert(
|
2026-02-08 16:45:52 -06:00
|
|
|
titleRes = Res.string.client_notification,
|
2025-07-25 07:07:01 -05:00
|
|
|
message = it,
|
2025-09-25 09:57:26 -04:00
|
|
|
onConfirm = { serviceRepository.clearErrorMessage() },
|
2026-02-08 16:45:52 -06:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.launchIn(viewModelScope)
|
|
|
|
|
|
|
|
|
|
serviceRepository.clientNotification
|
|
|
|
|
.filterNotNull()
|
|
|
|
|
.onEach { notification ->
|
|
|
|
|
val isCompromised = notification.low_entropy_key != null || notification.duplicated_public_key != null
|
|
|
|
|
showAlert(
|
|
|
|
|
titleRes = Res.string.client_notification,
|
|
|
|
|
message = if (isCompromised) getString(Res.string.compromised_keys) else notification.message,
|
|
|
|
|
onConfirm = {
|
|
|
|
|
// Action for compromised keys should be handled via a callback or event
|
|
|
|
|
clearClientNotification(notification)
|
|
|
|
|
},
|
|
|
|
|
onDismiss = { clearClientNotification(notification) },
|
2025-07-25 07:07:01 -05:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.launchIn(viewModelScope)
|
|
|
|
|
|
2025-12-28 08:30:15 -06:00
|
|
|
Logger.d { "ViewModel created" }
|
2020-04-07 17:42:31 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 18:01:12 -06:00
|
|
|
private val _sharedContactRequested: MutableStateFlow<SharedContact?> = MutableStateFlow(null)
|
|
|
|
|
val sharedContactRequested: StateFlow<SharedContact?>
|
2025-07-25 07:07:01 -05:00
|
|
|
get() = _sharedContactRequested.asStateFlow()
|
|
|
|
|
|
2026-02-08 16:45:52 -06:00
|
|
|
fun setSharedContactRequested(contact: SharedContact?) {
|
|
|
|
|
_sharedContactRequested.value = contact
|
2025-10-03 06:42:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Called immediately after activity observes requestChannelUrl */
|
|
|
|
|
fun clearSharedContactRequested() {
|
|
|
|
|
_sharedContactRequested.value = null
|
2025-07-03 12:42:04 +00:00
|
|
|
}
|
|
|
|
|
|
2023-10-21 07:24:46 -03:00
|
|
|
// Connection state to our radio device
|
2025-07-25 07:07:01 -05:00
|
|
|
val connectionState
|
2025-09-25 09:57:26 -04:00
|
|
|
get() = serviceRepository.connectionState
|
2025-07-25 07:07:01 -05:00
|
|
|
|
2026-02-03 18:01:12 -06:00
|
|
|
private val _requestChannelSet = MutableStateFlow<ChannelSet?>(null)
|
|
|
|
|
val requestChannelSet: StateFlow<ChannelSet?>
|
2025-07-25 07:07:01 -05:00
|
|
|
get() = _requestChannelSet
|
2022-05-17 17:29:21 -03:00
|
|
|
|
2026-02-08 16:45:52 -06:00
|
|
|
fun setRequestChannelSet(channelSet: ChannelSet?) {
|
|
|
|
|
_requestChannelSet.value = channelSet
|
|
|
|
|
}
|
2022-05-17 17:29:21 -03:00
|
|
|
|
2026-02-05 22:16:16 -06:00
|
|
|
/** Unified handler for scanned Meshtastic URIs (contacts or channels). */
|
|
|
|
|
fun handleScannedUri(uri: Uri, onInvalid: () -> Unit) {
|
2026-02-08 16:45:52 -06:00
|
|
|
uri.dispatchMeshtasticUri(
|
|
|
|
|
onContact = { setSharedContactRequested(it) },
|
|
|
|
|
onChannel = { setRequestChannelSet(it) },
|
|
|
|
|
onInvalid = onInvalid,
|
|
|
|
|
)
|
2026-02-05 22:16:16 -06:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
val latestStableFirmwareRelease = firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() }
|
2025-06-03 19:58:21 -05:00
|
|
|
|
2025-07-25 07:07:01 -05:00
|
|
|
/** Called immediately after activity observes requestChannelUrl */
|
2022-05-17 17:29:21 -03:00
|
|
|
fun clearRequestChannelUrl() {
|
2024-11-20 18:40:24 -03:00
|
|
|
_requestChannelSet.value = null
|
2022-05-17 17:29:21 -03:00
|
|
|
}
|
|
|
|
|
|
2020-04-08 09:53:04 -07:00
|
|
|
override fun onCleared() {
|
|
|
|
|
super.onCleared()
|
2025-12-28 08:30:15 -06:00
|
|
|
Logger.d { "ViewModel cleared" }
|
2020-04-08 09:53:04 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 16:45:52 -06:00
|
|
|
val tracerouteResponse: Flow<TracerouteResponse?>
|
|
|
|
|
get() = serviceRepository.tracerouteResponse
|
2023-09-16 09:51:16 -03:00
|
|
|
|
|
|
|
|
fun clearTracerouteResponse() {
|
2025-09-25 09:57:26 -04:00
|
|
|
serviceRepository.clearTracerouteResponse()
|
2023-09-16 09:51:16 -03:00
|
|
|
}
|
2024-02-13 14:32:52 -07:00
|
|
|
|
2026-02-20 06:41:52 -06:00
|
|
|
val neighborInfoResponse: StateFlow<String?> = serviceRepository.neighborInfoResponse
|
2025-12-22 07:45:06 +11:00
|
|
|
|
|
|
|
|
fun clearNeighborInfoResponse() {
|
|
|
|
|
serviceRepository.clearNeighborInfoResponse()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 15:52:09 -04:00
|
|
|
val appIntroCompleted: StateFlow<Boolean> = uiPreferencesDataSource.appIntroCompleted
|
2025-08-16 14:58:21 -04:00
|
|
|
|
|
|
|
|
fun onAppIntroCompleted() {
|
2025-09-23 15:52:09 -04:00
|
|
|
uiPreferencesDataSource.setAppIntroCompleted(true)
|
2025-08-16 14:58:21 -04:00
|
|
|
}
|
2025-10-06 14:30:18 -04:00
|
|
|
|
2025-10-07 15:35:44 -04:00
|
|
|
@Composable
|
|
|
|
|
fun AddNavigationTrackingEffect(navController: NavHostController) {
|
|
|
|
|
analytics.AddNavigationTrackingEffect(navController)
|
2025-10-06 14:30:18 -04:00
|
|
|
}
|
2023-09-16 09:51:16 -03:00
|
|
|
}
|