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:
niccellular
2026-03-03 17:17:24 -05:00
parent e7ba8e8497
commit 1ea87dbbad
19 changed files with 135 additions and 135 deletions

View File

@@ -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()
}
/**

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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?) {

View File

@@ -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),

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()
}
}
}

View File

@@ -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))

View File

@@ -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 }

View File

@@ -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()

View File

@@ -48,7 +48,7 @@ constructor(
val connectionState = serviceRepository.connectionState
val takLockState = serviceRepository.takLockState
val lockdownState = serviceRepository.lockdownState
val myNodeInfo: StateFlow<MyNodeEntity?> = nodeRepository.myNodeInfo

View File

@@ -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();
}

View File

@@ -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.

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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) } }

View File

@@ -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 {