mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-26 17:51:32 -04:00
Rename from Tak centric to "Lockdown" mode
3 files renamed (via git mv, history preserved): - TakLockHandler.kt → LockdownHandler.kt - TakPassphraseStore.kt → LockdownPassphraseStore.kt - TakUnlockDialog.kt → LockdownUnlockDialog.kt 16 files updated with consistent renames across the entire codebase. No stray TAK-named symbols remain in any .kt or .aidl source file. What stayed the same (wire protocol / firmware-defined): - The firmware notification strings: "TAK_LOCKED", "TAK_NEEDS_PROVISION", "TAK_UNLOCKED", "TAK_UNLOCK_FAILED" — still matched as string literals in LockdownHandler.kt - Config.DeviceConfig.Role.TAK / TAK_TRACKER proto enum values - The SharedPrefs key changed from "tak_passphrase_store" → "lockdown_passphrase_store" (existing stored passphrases won't migrate automatically — users will need to re-enter on first launch of the updated app)
This commit is contained in:
@@ -58,8 +58,8 @@ import org.meshtastic.core.model.util.toChannelSet
|
||||
import org.meshtastic.core.service.IMeshService
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.TakLockState
|
||||
import org.meshtastic.core.service.TakTokenInfo
|
||||
import org.meshtastic.core.service.LockdownState
|
||||
import org.meshtastic.core.service.LockdownTokenInfo
|
||||
import org.meshtastic.core.service.TracerouteResponse
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.client_notification
|
||||
@@ -132,19 +132,19 @@ constructor(
|
||||
meshServiceNotifications.clearClientNotification(notification)
|
||||
}
|
||||
|
||||
val takLockState: StateFlow<TakLockState> = serviceRepository.takLockState
|
||||
val takTokenInfo: StateFlow<TakTokenInfo?> = serviceRepository.takTokenInfo
|
||||
val lockdownState: StateFlow<LockdownState> = serviceRepository.lockdownState
|
||||
val lockdownTokenInfo: StateFlow<LockdownTokenInfo?> = serviceRepository.lockdownTokenInfo
|
||||
|
||||
fun sendTakUnlock(passphrase: String, bootTtl: Int = DEFAULT_BOOT_TTL, hourTtl: Int = 0) {
|
||||
serviceRepository.meshService?.sendTakUnlock(passphrase, bootTtl, hourTtl)
|
||||
fun sendLockdownUnlock(passphrase: String, bootTtl: Int = DEFAULT_BOOT_TTL, hourTtl: Int = 0) {
|
||||
serviceRepository.meshService?.sendLockdownUnlock(passphrase, bootTtl, hourTtl)
|
||||
}
|
||||
|
||||
fun sendTakLockNow() {
|
||||
serviceRepository.meshService?.sendTakLockNow()
|
||||
fun sendLockNow() {
|
||||
serviceRepository.meshService?.sendLockNow()
|
||||
}
|
||||
|
||||
fun clearTakLockState() {
|
||||
serviceRepository.clearTakLockState()
|
||||
fun clearLockdownState() {
|
||||
serviceRepository.clearLockdownState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,7 +59,7 @@ constructor(
|
||||
}
|
||||
configCompleteId != null -> {
|
||||
router.configFlowManager.handleConfigComplete(configCompleteId)
|
||||
router.takLockHandler.onConfigComplete()
|
||||
router.lockdownHandler.onConfigComplete()
|
||||
}
|
||||
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
|
||||
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)
|
||||
@@ -69,7 +69,7 @@ constructor(
|
||||
clientNotification != null -> {
|
||||
val msg = clientNotification.message
|
||||
if (msg.startsWith("TAK_")) {
|
||||
router.takLockHandler.handleTakNotification(msg)
|
||||
router.lockdownHandler.handleLockdownNotification(msg)
|
||||
} else {
|
||||
serviceRepository.setClientNotification(clientNotification)
|
||||
serviceNotifications.showClientNotification(clientNotification)
|
||||
|
||||
@@ -19,24 +19,24 @@ package com.geeksville.mesh.service
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import dagger.Lazy
|
||||
import org.meshtastic.core.service.LockdownState
|
||||
import org.meshtastic.core.service.LockdownTokenInfo
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.service.TakLockState
|
||||
import org.meshtastic.core.service.TakTokenInfo
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class TakLockHandler @Inject constructor(
|
||||
class LockdownHandler @Inject constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val commandSender: MeshCommandSender,
|
||||
private val passphraseStore: TakPassphraseStore,
|
||||
private val passphraseStore: LockdownPassphraseStore,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val connectionManager: Lazy<MeshConnectionManager>,
|
||||
) {
|
||||
@Volatile private var wasAutoAttempt = false
|
||||
|
||||
@Volatile private var pendingPassphrase: String? = null
|
||||
@Volatile private var pendingBoots: Int = TakPassphraseStore.DEFAULT_BOOTS
|
||||
@Volatile private var pendingBoots: Int = LockdownPassphraseStore.DEFAULT_BOOTS
|
||||
@Volatile private var pendingHours: Int = 0
|
||||
|
||||
/** Called when the BLE connection is established, before the first config request. */
|
||||
@@ -44,15 +44,15 @@ class TakLockHandler @Inject constructor(
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
wasAutoAttempt = false
|
||||
pendingPassphrase = null
|
||||
pendingBoots = TakPassphraseStore.DEFAULT_BOOTS
|
||||
pendingBoots = LockdownPassphraseStore.DEFAULT_BOOTS
|
||||
pendingHours = 0
|
||||
}
|
||||
|
||||
/** Called when the BLE connection is lost. */
|
||||
fun onDisconnect() {
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
serviceRepository.setTakTokenInfo(null)
|
||||
serviceRepository.setTakLockState(TakLockState.None)
|
||||
serviceRepository.setLockdownTokenInfo(null)
|
||||
serviceRepository.setLockdownState(LockdownState.None)
|
||||
wasAutoAttempt = false
|
||||
pendingPassphrase = null
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class TakLockHandler @Inject constructor(
|
||||
/**
|
||||
* Called on every config_complete_id. Once [sessionAuthorized] is true (set on TAK_UNLOCKED),
|
||||
* this is a no-op — preventing the startConfigOnly config_complete_id from triggering any
|
||||
* further TAK handling. The dialog state is driven entirely by clientNotifications.
|
||||
* further lockdown handling. The dialog state is driven entirely by clientNotifications.
|
||||
*/
|
||||
fun onConfigComplete() {
|
||||
// Session already authenticated — this config_complete_id is from the startConfigOnly()
|
||||
@@ -69,13 +69,13 @@ class TakLockHandler @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes incoming TAK clientNotification messages:
|
||||
* Routes incoming lockdown clientNotification messages:
|
||||
* - TAK_NEEDS_PROVISION → device has no passphrase → show "Set Passphrase" dialog
|
||||
* - TAK_LOCKED:<reason> → device is locked → auto-unlock with stored passphrase or show dialog
|
||||
* - TAK_UNLOCKED → accepted; save passphrase, authorize session, re-sync config
|
||||
* - TAK_UNLOCK_FAILED → wrong passphrase; clear stored or increment retry counter
|
||||
*/
|
||||
fun handleTakNotification(message: String?) {
|
||||
fun handleLockdownNotification(message: String?) {
|
||||
when {
|
||||
message == TAK_NEEDS_PROVISION -> handleNeedsProvision()
|
||||
// Exact "TAK_LOCKED" = Lock Now was acknowledged by the device → re-lock the session.
|
||||
@@ -88,9 +88,9 @@ class TakLockHandler @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleLockNowAcknowledged() {
|
||||
Logger.i { "TAK: Lock Now acknowledged — resetting session authorization" }
|
||||
Logger.i { "Lockdown: Lock Now acknowledged — resetting session authorization" }
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
// Do NOT clear takTokenInfo here — keep it so the dialog pre-fills with the last-known
|
||||
// Do NOT clear lockdownTokenInfo here — keep it so the dialog pre-fills with the last-known
|
||||
// TTL values. It is refreshed by the next TAK_UNLOCKED response.
|
||||
wasAutoAttempt = false
|
||||
pendingPassphrase = null
|
||||
@@ -98,7 +98,7 @@ class TakLockHandler @Inject constructor(
|
||||
// The fresh config is loaded in handleUnlocked() after successful re-authentication.
|
||||
connectionManager.get().clearRadioConfig()
|
||||
// Signal the UI to disconnect — no dialog, just drop the connection.
|
||||
serviceRepository.setTakLockState(TakLockState.LockNowAcknowledged)
|
||||
serviceRepository.setLockdownState(LockdownState.LockNowAcknowledged)
|
||||
}
|
||||
|
||||
private fun handleLocked() {
|
||||
@@ -106,17 +106,17 @@ class TakLockHandler @Inject constructor(
|
||||
if (deviceAddress != null) {
|
||||
val stored = passphraseStore.getPassphrase(deviceAddress)
|
||||
if (stored != null) {
|
||||
Logger.i { "TAK: Auto-unlocking (TAK_LOCKED) with stored passphrase for $deviceAddress" }
|
||||
Logger.i { "Lockdown: Auto-unlocking (TAK_LOCKED) with stored passphrase for $deviceAddress" }
|
||||
wasAutoAttempt = true
|
||||
commandSender.sendTakPassphrase(stored.passphrase, stored.boots, stored.hours)
|
||||
commandSender.sendLockdownPassphrase(stored.passphrase, stored.boots, stored.hours)
|
||||
return
|
||||
}
|
||||
}
|
||||
serviceRepository.setTakLockState(TakLockState.Locked)
|
||||
serviceRepository.setLockdownState(LockdownState.Locked)
|
||||
}
|
||||
|
||||
private fun handleNeedsProvision() {
|
||||
serviceRepository.setTakLockState(TakLockState.NeedsProvision)
|
||||
serviceRepository.setLockdownState(LockdownState.NeedsProvision)
|
||||
}
|
||||
|
||||
private fun handleUnlocked(message: String) {
|
||||
@@ -124,11 +124,11 @@ class TakLockHandler @Inject constructor(
|
||||
val passphrase = pendingPassphrase
|
||||
if (deviceAddress != null && passphrase != null) {
|
||||
passphraseStore.savePassphrase(deviceAddress, passphrase, pendingBoots, pendingHours)
|
||||
Logger.i { "TAK: Saved passphrase for $deviceAddress" }
|
||||
Logger.i { "Lockdown: Saved passphrase for $deviceAddress" }
|
||||
}
|
||||
pendingPassphrase = null
|
||||
serviceRepository.setTakTokenInfo(parseTokenInfo(message))
|
||||
serviceRepository.setTakLockState(TakLockState.Unlocked)
|
||||
serviceRepository.setLockdownTokenInfo(parseTokenInfo(message))
|
||||
serviceRepository.setLockdownState(LockdownState.Unlocked)
|
||||
// Mark session authorized BEFORE calling startConfigOnly(). When the resulting
|
||||
// config_complete_id arrives, onConfigComplete() will see sessionAuthorized=true and return
|
||||
// immediately — no passphrase re-send, no loop.
|
||||
@@ -137,7 +137,7 @@ class TakLockHandler @Inject constructor(
|
||||
}
|
||||
|
||||
/** Parses boots= and until= fields from TAK_UNLOCKED:boots=N:until=EPOCH: */
|
||||
private fun parseTokenInfo(message: String): TakTokenInfo? {
|
||||
private fun parseTokenInfo(message: String): LockdownTokenInfo? {
|
||||
var boots = -1
|
||||
var until = 0L
|
||||
for (segment in message.split(":")) {
|
||||
@@ -146,7 +146,7 @@ class TakLockHandler @Inject constructor(
|
||||
segment.startsWith("until=") -> until = segment.removePrefix("until=").toLongOrNull() ?: 0L
|
||||
}
|
||||
}
|
||||
return if (boots >= 0) TakTokenInfo(boots, until) else null
|
||||
return if (boots >= 0) LockdownTokenInfo(boots, until) else null
|
||||
}
|
||||
|
||||
private fun handleUnlockFailed(message: String) {
|
||||
@@ -159,25 +159,25 @@ class TakLockHandler @Inject constructor(
|
||||
wasAutoAttempt = false
|
||||
if (backoffSeconds != null && backoffSeconds > 0) {
|
||||
// Rate-limited — stored passphrase may still be correct; keep it and show countdown.
|
||||
Logger.i { "TAK: Auto-unlock rate-limited (backoff=${backoffSeconds}s)" }
|
||||
serviceRepository.setTakLockState(TakLockState.UnlockBackoff(backoffSeconds))
|
||||
Logger.i { "Lockdown: Auto-unlock rate-limited (backoff=${backoffSeconds}s)" }
|
||||
serviceRepository.setLockdownState(LockdownState.UnlockBackoff(backoffSeconds))
|
||||
} else {
|
||||
// Wrong passphrase — clear stored passphrase.
|
||||
val deviceAddress = radioInterfaceService.getDeviceAddress()
|
||||
if (deviceAddress != null) {
|
||||
passphraseStore.clearPassphrase(deviceAddress)
|
||||
Logger.i { "TAK: Auto-unlock failed (wrong passphrase), cleared stored passphrase for $deviceAddress" }
|
||||
Logger.i { "Lockdown: Auto-unlock failed (wrong passphrase), cleared stored passphrase for $deviceAddress" }
|
||||
}
|
||||
serviceRepository.setTakLockState(TakLockState.Locked)
|
||||
serviceRepository.setLockdownState(LockdownState.Locked)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Manual attempt.
|
||||
if (backoffSeconds != null && backoffSeconds > 0) {
|
||||
Logger.i { "TAK: Unlock failed with backoff of ${backoffSeconds}s" }
|
||||
serviceRepository.setTakLockState(TakLockState.UnlockBackoff(backoffSeconds))
|
||||
Logger.i { "Lockdown: Unlock failed with backoff of ${backoffSeconds}s" }
|
||||
serviceRepository.setLockdownState(LockdownState.UnlockBackoff(backoffSeconds))
|
||||
} else {
|
||||
serviceRepository.setTakLockState(TakLockState.UnlockFailed)
|
||||
serviceRepository.setLockdownState(LockdownState.UnlockFailed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,12 +186,12 @@ class TakLockHandler @Inject constructor(
|
||||
pendingBoots = boots
|
||||
pendingHours = hours
|
||||
wasAutoAttempt = false
|
||||
serviceRepository.setTakLockState(TakLockState.None) // hide dialog while awaiting response
|
||||
commandSender.sendTakPassphrase(passphrase, boots, hours)
|
||||
serviceRepository.setLockdownState(LockdownState.None) // hide dialog while awaiting response
|
||||
commandSender.sendLockdownPassphrase(passphrase, boots, hours)
|
||||
}
|
||||
|
||||
fun lockNow() {
|
||||
commandSender.sendTakLockNow()
|
||||
commandSender.sendLockNow()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -30,7 +30,7 @@ data class StoredPassphrase(
|
||||
)
|
||||
|
||||
@Singleton
|
||||
class TakPassphraseStore @Inject constructor(app: Application) {
|
||||
class LockdownPassphraseStore @Inject constructor(app: Application) {
|
||||
|
||||
private val prefs: SharedPreferences by lazy {
|
||||
val masterKey = MasterKey.Builder(app)
|
||||
@@ -74,7 +74,7 @@ class TakPassphraseStore @Inject constructor(app: Application) {
|
||||
private fun sanitizeKey(address: String): String = address.replace(":", "_")
|
||||
|
||||
companion object {
|
||||
private const val PREFS_FILE_NAME = "tak_passphrase_store"
|
||||
private const val PREFS_FILE_NAME = "lockdown_passphrase_store"
|
||||
const val DEFAULT_BOOTS = 50
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ constructor(
|
||||
private val databaseManager: DatabaseManager,
|
||||
private val serviceNotifications: MeshServiceNotifications,
|
||||
private val messageProcessor: Lazy<MeshMessageProcessor>,
|
||||
private val takLockHandler: TakLockHandler,
|
||||
private val lockdownHandler: LockdownHandler,
|
||||
) {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
@@ -331,12 +331,12 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSendTakUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
|
||||
takLockHandler.submitPassphrase(passphrase, bootTtl, hourTtl)
|
||||
fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
|
||||
lockdownHandler.submitPassphrase(passphrase, bootTtl, hourTtl)
|
||||
}
|
||||
|
||||
fun handleSendTakLockNow() {
|
||||
takLockHandler.lockNow()
|
||||
fun handleSendLockNow() {
|
||||
lockdownHandler.lockNow()
|
||||
}
|
||||
|
||||
fun handleUpdateLastAddress(deviceAddr: String?) {
|
||||
|
||||
@@ -469,7 +469,7 @@ constructor(
|
||||
),
|
||||
)
|
||||
|
||||
fun sendTakPassphrase(passphrase: String, boots: Int = 0, hours: Int = 0) {
|
||||
fun sendLockdownPassphrase(passphrase: String, boots: Int = 0, hours: Int = 0) {
|
||||
val myNum = nodeManager?.myNodeNum ?: return
|
||||
// The firmware expects slot 2 as an absolute Unix epoch (seconds), not a duration.
|
||||
// Convert hours duration → absolute epoch; 0 hours means no time-based expiry (until=0).
|
||||
@@ -509,7 +509,7 @@ constructor(
|
||||
packetHandler?.sendToRadio(ToRadio(packet = packet))
|
||||
}
|
||||
|
||||
fun sendTakLockNow() {
|
||||
fun sendLockNow() {
|
||||
val myNum = nodeManager?.myNodeNum ?: return
|
||||
val securityConfig = Config.SecurityConfig(
|
||||
private_key = ByteString.of(TAK_LOCK_BYTE),
|
||||
|
||||
@@ -70,7 +70,7 @@ constructor(
|
||||
private val commandSender: MeshCommandSender,
|
||||
private val nodeManager: MeshNodeManager,
|
||||
private val analytics: PlatformAnalytics,
|
||||
private val takLockHandler: Lazy<TakLockHandler>,
|
||||
private val lockdownHandler: Lazy<LockdownHandler>,
|
||||
) {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var sleepTimeout: Job? = null
|
||||
@@ -144,7 +144,7 @@ constructor(
|
||||
Logger.d { "Starting connect" }
|
||||
connectTimeMsec = System.currentTimeMillis()
|
||||
scope.handledLaunch { nodeRepository.clearMyNodeInfo() }
|
||||
takLockHandler.get().onConnect()
|
||||
lockdownHandler.get().onConnect()
|
||||
startConfigOnly()
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ constructor(
|
||||
|
||||
private fun handleDisconnected() {
|
||||
connectionStateHolder.setState(ConnectionState.Disconnected)
|
||||
takLockHandler.get().onDisconnect()
|
||||
lockdownHandler.get().onDisconnect()
|
||||
packetHandler.stopPacketQueue()
|
||||
locationManager.stop()
|
||||
mqttManager.stop()
|
||||
|
||||
@@ -36,7 +36,7 @@ constructor(
|
||||
val configFlowManager: MeshConfigFlowManager,
|
||||
val mqttManager: MeshMqttManager,
|
||||
val actionHandler: MeshActionHandler,
|
||||
val takLockHandler: TakLockHandler,
|
||||
val lockdownHandler: LockdownHandler,
|
||||
) {
|
||||
fun start(scope: CoroutineScope) {
|
||||
dataHandler.start(scope)
|
||||
|
||||
@@ -379,12 +379,12 @@ class MeshService : Service() {
|
||||
router.actionHandler.handleRequestRebootOta(requestId, destNum, mode, hash)
|
||||
}
|
||||
|
||||
override fun sendTakUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) = toRemoteExceptions {
|
||||
router.actionHandler.handleSendTakUnlock(passphrase, bootTtl, hourTtl)
|
||||
override fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) = toRemoteExceptions {
|
||||
router.actionHandler.handleSendLockdownUnlock(passphrase, bootTtl, hourTtl)
|
||||
}
|
||||
|
||||
override fun sendTakLockNow() = toRemoteExceptions {
|
||||
router.actionHandler.handleSendTakLockNow()
|
||||
override fun sendLockNow() = toRemoteExceptions {
|
||||
router.actionHandler.handleSendLockNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,22 +46,22 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.service.TakLockState
|
||||
import org.meshtastic.core.service.TakTokenInfo
|
||||
import org.meshtastic.core.service.LockdownState
|
||||
import org.meshtastic.core.service.LockdownTokenInfo
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun TakUnlockDialog(
|
||||
takLockState: TakLockState,
|
||||
takTokenInfo: TakTokenInfo? = null,
|
||||
fun LockdownUnlockDialog(
|
||||
lockdownState: LockdownState,
|
||||
lockdownTokenInfo: LockdownTokenInfo? = null,
|
||||
onSubmit: (passphrase: String, boots: Int, hours: Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val shouldShow = when (takLockState) {
|
||||
is TakLockState.Locked -> true
|
||||
is TakLockState.NeedsProvision -> true
|
||||
is TakLockState.UnlockFailed -> true
|
||||
is TakLockState.UnlockBackoff -> true
|
||||
val shouldShow = when (lockdownState) {
|
||||
is LockdownState.Locked -> true
|
||||
is LockdownState.NeedsProvision -> true
|
||||
is LockdownState.UnlockFailed -> true
|
||||
is LockdownState.UnlockBackoff -> true
|
||||
else -> false
|
||||
}
|
||||
BackHandler(enabled = shouldShow, onBack = onDismiss)
|
||||
@@ -70,9 +70,9 @@ fun TakUnlockDialog(
|
||||
var passphrase by rememberSaveable { mutableStateOf("") }
|
||||
var passwordVisible by rememberSaveable { mutableStateOf(false) }
|
||||
// Pre-fill from most recent TAK_UNLOCKED token info when available.
|
||||
val initialBoots = takTokenInfo?.bootsRemaining ?: DEFAULT_BOOTS
|
||||
val initialHours = if ((takTokenInfo?.expiryEpoch ?: 0L) > 0L) {
|
||||
((takTokenInfo!!.expiryEpoch - System.currentTimeMillis() / 1000) / 3600)
|
||||
val initialBoots = lockdownTokenInfo?.bootsRemaining ?: DEFAULT_BOOTS
|
||||
val initialHours = if ((lockdownTokenInfo?.expiryEpoch ?: 0L) > 0L) {
|
||||
((lockdownTokenInfo!!.expiryEpoch - System.currentTimeMillis() / 1000) / 3600)
|
||||
.toInt().coerceAtLeast(0)
|
||||
} else {
|
||||
0
|
||||
@@ -80,7 +80,7 @@ fun TakUnlockDialog(
|
||||
var boots by rememberSaveable { mutableIntStateOf(initialBoots) }
|
||||
var hours by rememberSaveable { mutableIntStateOf(initialHours) }
|
||||
|
||||
val isProvisioning = takLockState is TakLockState.NeedsProvision
|
||||
val isProvisioning = lockdownState is LockdownState.NeedsProvision
|
||||
val title = if (isProvisioning) "Set Passphrase" else "Enter Passphrase"
|
||||
val isValid = passphrase.isNotEmpty() && passphrase.length <= MAX_PASSPHRASE_LEN
|
||||
|
||||
@@ -89,17 +89,17 @@ fun TakUnlockDialog(
|
||||
title = { Text(text = title) },
|
||||
text = {
|
||||
Column {
|
||||
when (takLockState) {
|
||||
is TakLockState.UnlockFailed -> {
|
||||
when (lockdownState) {
|
||||
is LockdownState.UnlockFailed -> {
|
||||
Text(
|
||||
text = "Incorrect passphrase.",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(SPACING_DP.dp))
|
||||
}
|
||||
is TakLockState.UnlockBackoff -> {
|
||||
is LockdownState.UnlockBackoff -> {
|
||||
Text(
|
||||
text = "Try again in ${takLockState.backoffSeconds} seconds.",
|
||||
text = "Try again in ${lockdownState.backoffSeconds} seconds.",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(SPACING_DP.dp))
|
||||
@@ -113,7 +113,7 @@ import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.TakLockState
|
||||
import org.meshtastic.core.service.LockdownState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.app_too_old
|
||||
import org.meshtastic.core.strings.bottom_nav_settings
|
||||
@@ -218,11 +218,11 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
||||
}
|
||||
}
|
||||
|
||||
val takLockState by uIViewModel.takLockState.collectAsStateWithLifecycle()
|
||||
val takTokenInfo by uIViewModel.takTokenInfo.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(takLockState) {
|
||||
if (takLockState is TakLockState.LockNowAcknowledged) {
|
||||
uIViewModel.clearTakLockState()
|
||||
val lockdownState by uIViewModel.lockdownState.collectAsStateWithLifecycle()
|
||||
val lockdownTokenInfo by uIViewModel.lockdownTokenInfo.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(lockdownState) {
|
||||
if (lockdownState is LockdownState.LockNowAcknowledged) {
|
||||
uIViewModel.clearLockdownState()
|
||||
scanModel.disconnect()
|
||||
navController.navigate(TopLevelDestination.Connections.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
@@ -231,12 +231,12 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
||||
}
|
||||
}
|
||||
}
|
||||
TakUnlockDialog(
|
||||
takLockState = takLockState,
|
||||
takTokenInfo = takTokenInfo,
|
||||
onSubmit = { pass, boots, hours -> uIViewModel.sendTakUnlock(pass, boots, hours) },
|
||||
LockdownUnlockDialog(
|
||||
lockdownState = lockdownState,
|
||||
lockdownTokenInfo = lockdownTokenInfo,
|
||||
onSubmit = { pass, boots, hours -> uIViewModel.sendLockdownUnlock(pass, boots, hours) },
|
||||
onDismiss = {
|
||||
uIViewModel.clearTakLockState()
|
||||
uIViewModel.clearLockdownState()
|
||||
scanModel.disconnect()
|
||||
navController.navigate(TopLevelDestination.Connections.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
|
||||
|
||||
@@ -70,7 +70,7 @@ import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.core.service.TakLockState
|
||||
import org.meshtastic.core.service.LockdownState
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.connected
|
||||
import org.meshtastic.core.strings.connected_device
|
||||
@@ -126,10 +126,10 @@ fun ConnectionsScreen(
|
||||
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
|
||||
val bluetoothState by connectionsViewModel.bluetoothState.collectAsStateWithLifecycle()
|
||||
val takLockState by connectionsViewModel.takLockState.collectAsStateWithLifecycle()
|
||||
// A TAK-locked device sends zeroed config before auth — suppress region-unset until authorized.
|
||||
val isTakAuthorized = takLockState == TakLockState.None || takLockState == TakLockState.Unlocked
|
||||
val regionUnset = config.lora?.region == Config.LoRaConfig.RegionCode.UNSET && isTakAuthorized
|
||||
val lockdownState by connectionsViewModel.lockdownState.collectAsStateWithLifecycle()
|
||||
// A lockdown-mode device sends zeroed config before auth — suppress region-unset until authorized.
|
||||
val isLockdownAuthorized = lockdownState == LockdownState.None || lockdownState == LockdownState.Unlocked
|
||||
val regionUnset = config.lora?.region == Config.LoRaConfig.RegionCode.UNSET && isLockdownAuthorized
|
||||
|
||||
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
|
||||
val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -48,7 +48,7 @@ constructor(
|
||||
|
||||
val connectionState = serviceRepository.connectionState
|
||||
|
||||
val takLockState = serviceRepository.takLockState
|
||||
val lockdownState = serviceRepository.lockdownState
|
||||
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?> = nodeRepository.myNodeInfo
|
||||
|
||||
|
||||
@@ -190,9 +190,9 @@ interface IMeshService {
|
||||
*/
|
||||
void requestRebootOta(in int requestId, in int destNum, in int mode, in byte []hash);
|
||||
|
||||
/// Send TAK unlock passphrase to the device
|
||||
void sendTakUnlock(in String passphrase, in int bootTtl, in int hourTtl);
|
||||
/// Send lockdown unlock passphrase to the device
|
||||
void sendLockdownUnlock(in String passphrase, in int bootTtl, in int hourTtl);
|
||||
|
||||
/// Lock the device with TAK lock immediately
|
||||
void sendTakLockNow();
|
||||
/// Lock the device immediately (lockdown mode)
|
||||
void sendLockNow();
|
||||
}
|
||||
|
||||
@@ -32,26 +32,26 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed class TakLockState {
|
||||
data object None : TakLockState()
|
||||
data object Locked : TakLockState()
|
||||
data object NeedsProvision : TakLockState()
|
||||
data object Unlocked : TakLockState()
|
||||
sealed class LockdownState {
|
||||
data object None : LockdownState()
|
||||
data object Locked : LockdownState()
|
||||
data object NeedsProvision : LockdownState()
|
||||
data object Unlocked : LockdownState()
|
||||
/** Lock Now ACK received — client should disconnect immediately, no dialog. */
|
||||
data object LockNowAcknowledged : TakLockState()
|
||||
data object LockNowAcknowledged : LockdownState()
|
||||
/** Wrong passphrase — retry immediately. */
|
||||
data object UnlockFailed : TakLockState()
|
||||
data object UnlockFailed : LockdownState()
|
||||
/** Too many attempts — must wait [backoffSeconds] before retrying. */
|
||||
data class UnlockBackoff(val backoffSeconds: Int) : TakLockState()
|
||||
data class UnlockBackoff(val backoffSeconds: Int) : LockdownState()
|
||||
}
|
||||
|
||||
/**
|
||||
* TAK session token metadata parsed from the TAK_UNLOCKED:boots=N:until=EPOCH: notification.
|
||||
* Lockdown session token metadata parsed from the TAK_UNLOCKED:boots=N:until=EPOCH: notification.
|
||||
*
|
||||
* @param bootsRemaining Number of reboots before the token expires.
|
||||
* @param expiryEpoch Unix epoch seconds; 0 means no time-based expiry.
|
||||
*/
|
||||
data class TakTokenInfo(
|
||||
data class LockdownTokenInfo(
|
||||
val bootsRemaining: Int,
|
||||
val expiryEpoch: Long,
|
||||
)
|
||||
@@ -184,26 +184,26 @@ class ServiceRepository @Inject constructor() {
|
||||
_serviceAction.send(action)
|
||||
}
|
||||
|
||||
// TAK lock state
|
||||
private val _takLockState: MutableStateFlow<TakLockState> = MutableStateFlow(TakLockState.None)
|
||||
val takLockState: StateFlow<TakLockState>
|
||||
get() = _takLockState
|
||||
// Lockdown state
|
||||
private val _lockdownState: MutableStateFlow<LockdownState> = MutableStateFlow(LockdownState.None)
|
||||
val lockdownState: StateFlow<LockdownState>
|
||||
get() = _lockdownState
|
||||
|
||||
fun setTakLockState(state: TakLockState) {
|
||||
_takLockState.value = state
|
||||
fun setLockdownState(state: LockdownState) {
|
||||
_lockdownState.value = state
|
||||
}
|
||||
|
||||
fun clearTakLockState() {
|
||||
_takLockState.value = TakLockState.None
|
||||
fun clearLockdownState() {
|
||||
_lockdownState.value = LockdownState.None
|
||||
}
|
||||
|
||||
// TAK token info (boots remaining + expiry) from the most recent TAK_UNLOCKED notification
|
||||
private val _takTokenInfo: MutableStateFlow<TakTokenInfo?> = MutableStateFlow(null)
|
||||
val takTokenInfo: StateFlow<TakTokenInfo?>
|
||||
get() = _takTokenInfo
|
||||
// Lockdown token info (boots remaining + expiry) from the most recent TAK_UNLOCKED notification
|
||||
private val _lockdownTokenInfo: MutableStateFlow<LockdownTokenInfo?> = MutableStateFlow(null)
|
||||
val lockdownTokenInfo: StateFlow<LockdownTokenInfo?>
|
||||
get() = _lockdownTokenInfo
|
||||
|
||||
fun setTakTokenInfo(info: TakTokenInfo?) {
|
||||
_takTokenInfo.value = info
|
||||
fun setLockdownTokenInfo(info: LockdownTokenInfo?) {
|
||||
_lockdownTokenInfo.value = info
|
||||
}
|
||||
|
||||
// True once TAK passphrase is accepted for this BLE connection; false on disconnect.
|
||||
|
||||
@@ -121,7 +121,7 @@ open class FakeIMeshService : IMeshService.Stub() {
|
||||
|
||||
override fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
|
||||
|
||||
override fun sendTakUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
|
||||
override fun sendTakLockNow() {}
|
||||
override fun sendLockNow() {}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ open class FakeIMeshService : IMeshService.Stub() {
|
||||
|
||||
override fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
|
||||
|
||||
override fun sendTakUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
|
||||
override fun sendTakLockNow() {}
|
||||
override fun sendLockNow() {}
|
||||
}
|
||||
|
||||
@@ -147,11 +147,11 @@ constructor(
|
||||
private val _radioConfigState = MutableStateFlow(RadioConfigState())
|
||||
val radioConfigState: StateFlow<RadioConfigState> = _radioConfigState
|
||||
|
||||
fun sendTakLockNow() {
|
||||
meshService?.sendTakLockNow()
|
||||
fun sendLockNow() {
|
||||
meshService?.sendLockNow()
|
||||
}
|
||||
|
||||
val takTokenInfo = serviceRepository.takTokenInfo
|
||||
val lockdownTokenInfo = serviceRepository.lockdownTokenInfo
|
||||
|
||||
fun setPreserveFavorites(preserveFavorites: Boolean) {
|
||||
viewModelScope.launch { _radioConfigState.update { it.copy(nodeDbResetPreserveFavorites = preserveFavorites) } }
|
||||
|
||||
@@ -87,7 +87,7 @@ import java.security.SecureRandom
|
||||
@Composable
|
||||
fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
val takTokenInfo by viewModel.takTokenInfo.collectAsStateWithLifecycle(initialValue = null)
|
||||
val lockdownTokenInfo by viewModel.lockdownTokenInfo.collectAsStateWithLifecycle(initialValue = null)
|
||||
val node by viewModel.destNode.collectAsStateWithLifecycle()
|
||||
val securityConfig = state.radioConfig.security ?: Config.SecurityConfig()
|
||||
val formState = rememberConfigState(initialValue = securityConfig)
|
||||
@@ -266,9 +266,9 @@ fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBa
|
||||
title = "Lock Now",
|
||||
enabled = state.connected,
|
||||
icon = Icons.TwoTone.Warning,
|
||||
onClick = { viewModel.sendTakLockNow() },
|
||||
onClick = { viewModel.sendLockNow() },
|
||||
)
|
||||
takTokenInfo?.let { token ->
|
||||
lockdownTokenInfo?.let { token ->
|
||||
HorizontalDivider()
|
||||
val expiryMs = token.expiryEpoch * 1000L
|
||||
val expiryText = when {
|
||||
|
||||
Reference in New Issue
Block a user