mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-03 14:06:34 +02:00
feat(lockdown): implement coordinator and typed status dispatch
- CommandSenderImpl: build AdminMessage.lockdown_auth = LockdownAuth(...) for provision/unlock and lock_now=true for the lock command. - FromRadioPacketHandlerImpl: route the new FromRadio.lockdown_status variant to the coordinator; also notify the coordinator on config_complete_id. - MeshActionHandlerImpl: forward handleSendLockdownUnlock/handleSendLockNow to the coordinator. - MeshConnectionManagerImpl: call coordinator.onConnect/onDisconnect; add clearRadioConfig to purge cached config after a lock-now ACK. - ServiceRepositoryImpl: back the lockdownState/lockdownTokenInfo/ sessionAuthorized flows. - LockdownHandlerImpl: orchestration. Switches on LockdownStatus.State (NEEDS_PROVISION / LOCKED / UNLOCKED / UNLOCK_FAILED), auto-replays stored passphrase on LOCKED, clears stored passphrase on a fresh UNLOCK_FAILED, surfaces backoff_seconds on rate-limit. Tracks a wasLockNow flag locally so the next LOCKED status after a lock-now command is translated to LockdownState.LockNowAcknowledged for an immediate UI disconnect (the new schema has no explicit ACK type). - LockdownPassphraseStore: per-device EncryptedSharedPreferences store for auto-unlock. Not biometric-gated by design. - Add androidx.security:security-crypto dependency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,11 +42,13 @@ import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Constants
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LockdownAuth
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Neighbor
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
@@ -355,6 +357,38 @@ class CommandSenderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendLockdownPassphrase(passphrase: String, boots: Int, hours: Int) {
|
||||
val validUntilEpoch =
|
||||
if (hours > 0) (nowMillis / 1000L + hours.toLong() * SECONDS_PER_HOUR).toInt() else 0
|
||||
val lockdownAuth =
|
||||
LockdownAuth(
|
||||
passphrase = passphrase.encodeToByteArray().toByteString(),
|
||||
boots_remaining = boots.coerceAtLeast(0),
|
||||
valid_until_epoch = validUntilEpoch,
|
||||
)
|
||||
sendLockdownAdmin(AdminMessage(lockdown_auth = lockdownAuth))
|
||||
}
|
||||
|
||||
override fun sendLockNow() {
|
||||
sendLockdownAdmin(AdminMessage(lockdown_auth = LockdownAuth(lock_now = true)))
|
||||
}
|
||||
|
||||
private fun sendLockdownAdmin(adminMessage: AdminMessage) {
|
||||
val myNum = nodeManager.myNodeNum ?: return
|
||||
val packet =
|
||||
MeshPacket(
|
||||
to = myNum,
|
||||
id = generatePacketId(),
|
||||
channel = 0,
|
||||
want_ack = true,
|
||||
hop_limit = DEFAULT_HOP_LIMIT,
|
||||
hop_start = DEFAULT_HOP_LIMIT,
|
||||
priority = MeshPacket.Priority.RELIABLE,
|
||||
decoded = Data(portnum = PortNum.ADMIN_APP, payload = adminMessage.encode().toByteString()),
|
||||
)
|
||||
packetHandler.sendToRadio(ToRadio(packet = packet))
|
||||
}
|
||||
|
||||
fun resolveNodeNum(toId: String): Int = when (toId) {
|
||||
DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST
|
||||
else -> {
|
||||
@@ -436,5 +470,7 @@ class CommandSenderImpl(
|
||||
private const val HEX_RADIX = 16
|
||||
|
||||
private const val DEFAULT_HOP_LIMIT = 3
|
||||
|
||||
private const val SECONDS_PER_HOUR = 3600
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -18,6 +18,7 @@ package org.meshtastic.core.data.manager
|
||||
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.repository.FromRadioPacketHandler
|
||||
import org.meshtastic.core.repository.LockdownCoordinator
|
||||
import org.meshtastic.core.repository.MeshRouter
|
||||
import org.meshtastic.core.repository.MqttManager
|
||||
import org.meshtastic.core.repository.Notification
|
||||
@@ -37,6 +38,7 @@ class FromRadioPacketHandlerImpl(
|
||||
private val mqttManager: MqttManager,
|
||||
private val packetHandler: PacketHandler,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val lockdownCoordinator: LockdownCoordinator,
|
||||
) : FromRadioPacketHandler {
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun handleFromRadio(proto: FromRadio) {
|
||||
@@ -50,6 +52,7 @@ class FromRadioPacketHandlerImpl(
|
||||
val moduleConfig = proto.moduleConfig
|
||||
val channel = proto.channel
|
||||
val clientNotification = proto.clientNotification
|
||||
val lockdownStatus = proto.lockdown_status
|
||||
|
||||
when {
|
||||
myInfo != null -> router.value.configFlowManager.handleMyInfo(myInfo)
|
||||
@@ -58,12 +61,16 @@ class FromRadioPacketHandlerImpl(
|
||||
router.value.configFlowManager.handleNodeInfo(nodeInfo)
|
||||
serviceRepository.setConnectionProgress("Nodes (${router.value.configFlowManager.newNodeCount})")
|
||||
}
|
||||
configCompleteId != null -> router.value.configFlowManager.handleConfigComplete(configCompleteId)
|
||||
configCompleteId != null -> {
|
||||
router.value.configFlowManager.handleConfigComplete(configCompleteId)
|
||||
lockdownCoordinator.onConfigComplete()
|
||||
}
|
||||
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
|
||||
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)
|
||||
config != null -> router.value.configHandler.handleDeviceConfig(config)
|
||||
moduleConfig != null -> router.value.configHandler.handleModuleConfig(moduleConfig)
|
||||
channel != null -> router.value.configHandler.handleChannel(channel)
|
||||
lockdownStatus != null -> lockdownCoordinator.handleLockdownStatus(lockdownStatus)
|
||||
clientNotification != null -> {
|
||||
serviceRepository.setClientNotification(clientNotification)
|
||||
notificationManager.dispatch(
|
||||
|
||||
+10
@@ -33,6 +33,7 @@ import org.meshtastic.core.model.Reaction
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.DataPair
|
||||
import org.meshtastic.core.repository.LockdownCoordinator
|
||||
import org.meshtastic.core.repository.MeshActionHandler
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshMessageProcessor
|
||||
@@ -63,6 +64,7 @@ class MeshActionHandlerImpl(
|
||||
private val databaseManager: DatabaseManager,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val messageProcessor: Lazy<MeshMessageProcessor>,
|
||||
private val lockdownCoordinator: LockdownCoordinator,
|
||||
) : MeshActionHandler {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
@@ -351,4 +353,12 @@ class MeshActionHandlerImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
|
||||
lockdownCoordinator.submitPassphrase(passphrase, bootTtl, hourTtl)
|
||||
}
|
||||
|
||||
override fun handleSendLockNow() {
|
||||
lockdownCoordinator.lockNow()
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -38,6 +38,7 @@ import org.meshtastic.core.repository.AppWidgetUpdater
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.DataPair
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.LockdownCoordinator
|
||||
import org.meshtastic.core.repository.MeshConnectionManager
|
||||
import org.meshtastic.core.repository.MeshLocationManager
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
@@ -88,6 +89,7 @@ class MeshConnectionManagerImpl(
|
||||
private val packetRepository: PacketRepository,
|
||||
private val workerManager: MeshWorkerManager,
|
||||
private val appWidgetUpdater: AppWidgetUpdater,
|
||||
private val lockdownCoordinator: LockdownCoordinator,
|
||||
) : MeshConnectionManager {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private var sleepTimeout: Job? = null
|
||||
@@ -182,6 +184,7 @@ class MeshConnectionManagerImpl(
|
||||
serviceBroadcasts.broadcastConnection()
|
||||
Logger.i { "Starting mesh handshake (Stage 1)" }
|
||||
connectTimeMsec = nowMillis
|
||||
lockdownCoordinator.onConnect()
|
||||
startConfigOnly()
|
||||
}
|
||||
|
||||
@@ -238,6 +241,7 @@ class MeshConnectionManagerImpl(
|
||||
|
||||
private fun handleDisconnected() {
|
||||
serviceRepository.setConnectionState(ConnectionState.Disconnected)
|
||||
lockdownCoordinator.onDisconnect()
|
||||
packetHandler.stopPacketQueue()
|
||||
locationManager.stop()
|
||||
mqttManager.stop()
|
||||
@@ -258,6 +262,14 @@ class MeshConnectionManagerImpl(
|
||||
action()
|
||||
}
|
||||
|
||||
override fun clearRadioConfig() {
|
||||
scope.handledLaunch {
|
||||
radioConfigRepository.clearLocalConfig()
|
||||
radioConfigRepository.clearChannelSet()
|
||||
radioConfigRepository.clearLocalModuleConfig()
|
||||
}
|
||||
}
|
||||
|
||||
override fun startNodeInfoOnly() {
|
||||
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = NODE_INFO_NONCE)) }
|
||||
startHandshakeStallGuard(2, action)
|
||||
|
||||
@@ -49,6 +49,7 @@ kotlin {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.workmanager)
|
||||
}
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.service
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.koin.core.annotation.Single
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.meshtastic.core.model.service.LockdownState
|
||||
import org.meshtastic.core.model.service.LockdownTokenInfo
|
||||
import org.meshtastic.core.repository.CommandSender
|
||||
import org.meshtastic.core.repository.LockdownCoordinator
|
||||
import org.meshtastic.core.repository.MeshConnectionManager
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.LockdownStatus
|
||||
|
||||
@Single(binds = [LockdownCoordinator::class])
|
||||
class LockdownHandlerImpl(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val commandSender: CommandSender,
|
||||
private val passphraseStore: LockdownPassphraseStore,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
) : LockdownCoordinator, KoinComponent {
|
||||
private val connectionManager: MeshConnectionManager by inject()
|
||||
@Volatile private var wasAutoAttempt = false
|
||||
|
||||
@Volatile private var wasLockNow = false
|
||||
|
||||
@Volatile private var pendingPassphrase: String? = null
|
||||
|
||||
@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. */
|
||||
override fun onConnect() {
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
wasAutoAttempt = false
|
||||
wasLockNow = false
|
||||
pendingPassphrase = null
|
||||
pendingBoots = LockdownPassphraseStore.DEFAULT_BOOTS
|
||||
pendingHours = 0
|
||||
}
|
||||
|
||||
/** Called when the BLE connection is lost. */
|
||||
override fun onDisconnect() {
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
serviceRepository.setLockdownTokenInfo(null)
|
||||
serviceRepository.setLockdownState(LockdownState.None)
|
||||
wasAutoAttempt = false
|
||||
wasLockNow = false
|
||||
pendingPassphrase = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on every config_complete_id. Once [sessionAuthorized] is true (set on UNLOCKED),
|
||||
* this is a no-op — preventing the startConfigOnly config_complete_id from triggering any
|
||||
* further lockdown handling.
|
||||
*/
|
||||
override fun onConfigComplete() {
|
||||
if (serviceRepository.sessionAuthorized.value) return
|
||||
}
|
||||
|
||||
/** Routes typed firmware [LockdownStatus] to per-state handlers. */
|
||||
override fun handleLockdownStatus(status: LockdownStatus) {
|
||||
when (status.state) {
|
||||
LockdownStatus.State.NEEDS_PROVISION -> handleNeedsProvision()
|
||||
LockdownStatus.State.LOCKED -> handleLocked(status.lock_reason)
|
||||
LockdownStatus.State.UNLOCKED -> handleUnlocked(status)
|
||||
LockdownStatus.State.UNLOCK_FAILED -> handleUnlockFailed(status.backoff_seconds)
|
||||
LockdownStatus.State.STATE_UNSPECIFIED -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLockNowAcknowledged() {
|
||||
Logger.i { "Lockdown: Lock Now acknowledged — resetting session authorization" }
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
wasAutoAttempt = false
|
||||
wasLockNow = false
|
||||
pendingPassphrase = null
|
||||
// Purge cached config; fresh config is loaded after successful re-authentication.
|
||||
connectionManager.clearRadioConfig()
|
||||
// Signal the UI to disconnect — no dialog, just drop the connection.
|
||||
serviceRepository.setLockdownState(LockdownState.LockNowAcknowledged)
|
||||
}
|
||||
|
||||
private fun handleLocked(lockReason: String) {
|
||||
if (wasLockNow) {
|
||||
handleLockNowAcknowledged()
|
||||
return
|
||||
}
|
||||
val deviceAddress = radioInterfaceService.getDeviceAddress()
|
||||
if (deviceAddress != null) {
|
||||
val stored = passphraseStore.getPassphrase(deviceAddress)
|
||||
if (stored != null) {
|
||||
Logger.i { "Lockdown: Auto-unlocking (reason=$lockReason) with stored passphrase for $deviceAddress" }
|
||||
wasAutoAttempt = true
|
||||
commandSender.sendLockdownPassphrase(stored.passphrase, stored.boots, stored.hours)
|
||||
return
|
||||
}
|
||||
}
|
||||
serviceRepository.setLockdownState(LockdownState.Locked(lockReason))
|
||||
}
|
||||
|
||||
private fun handleNeedsProvision() {
|
||||
serviceRepository.setLockdownState(LockdownState.NeedsProvision)
|
||||
}
|
||||
|
||||
private fun handleUnlocked(status: LockdownStatus) {
|
||||
val deviceAddress = radioInterfaceService.getDeviceAddress()
|
||||
val passphrase = pendingPassphrase
|
||||
if (deviceAddress != null && passphrase != null) {
|
||||
passphraseStore.savePassphrase(deviceAddress, passphrase, pendingBoots, pendingHours)
|
||||
Logger.i { "Lockdown: Saved passphrase for $deviceAddress" }
|
||||
}
|
||||
pendingPassphrase = null
|
||||
serviceRepository.setLockdownTokenInfo(
|
||||
LockdownTokenInfo(
|
||||
bootsRemaining = status.boots_remaining,
|
||||
expiryEpoch = status.valid_until_epoch.toLong() and UINT32_MASK,
|
||||
),
|
||||
)
|
||||
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.
|
||||
serviceRepository.setSessionAuthorized(true)
|
||||
connectionManager.startConfigOnly()
|
||||
}
|
||||
|
||||
private fun handleUnlockFailed(backoffSeconds: Int) {
|
||||
pendingPassphrase = null
|
||||
if (wasAutoAttempt) {
|
||||
wasAutoAttempt = false
|
||||
if (backoffSeconds > 0) {
|
||||
Logger.i { "Lockdown: Auto-unlock rate-limited (backoff=${backoffSeconds}s)" }
|
||||
serviceRepository.setLockdownState(LockdownState.UnlockBackoff(backoffSeconds))
|
||||
} else {
|
||||
val deviceAddress = radioInterfaceService.getDeviceAddress()
|
||||
if (deviceAddress != null) {
|
||||
passphraseStore.clearPassphrase(deviceAddress)
|
||||
Logger.i { "Lockdown: Auto-unlock failed (wrong passphrase), cleared stored passphrase for $deviceAddress" }
|
||||
}
|
||||
serviceRepository.setLockdownState(LockdownState.Locked())
|
||||
}
|
||||
return
|
||||
}
|
||||
if (backoffSeconds > 0) {
|
||||
Logger.i { "Lockdown: Unlock failed with backoff of ${backoffSeconds}s" }
|
||||
serviceRepository.setLockdownState(LockdownState.UnlockBackoff(backoffSeconds))
|
||||
} else {
|
||||
serviceRepository.setLockdownState(LockdownState.UnlockFailed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun submitPassphrase(passphrase: String, boots: Int, hours: Int) {
|
||||
pendingPassphrase = passphrase
|
||||
pendingBoots = boots
|
||||
pendingHours = hours
|
||||
wasAutoAttempt = false
|
||||
wasLockNow = false
|
||||
serviceRepository.setLockdownState(LockdownState.None) // hide dialog while awaiting response
|
||||
commandSender.sendLockdownPassphrase(passphrase, boots, hours)
|
||||
}
|
||||
|
||||
override fun lockNow() {
|
||||
wasLockNow = true
|
||||
commandSender.sendLockNow()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UINT32_MASK = 0xFFFFFFFFL
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.service
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import org.koin.core.annotation.Single
|
||||
|
||||
data class StoredPassphrase(
|
||||
val passphrase: String,
|
||||
val boots: Int,
|
||||
val hours: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Encrypted per-device storage for lockdown passphrases.
|
||||
*
|
||||
* Uses EncryptedSharedPreferences backed by an AES-256-GCM MasterKey (hardware keystore when
|
||||
* available). The key is intentionally NOT gated behind biometric authentication so that
|
||||
* auto-unlock can run in the background without user interaction.
|
||||
*/
|
||||
@Single
|
||||
class LockdownPassphraseStore(app: Application) {
|
||||
|
||||
private val prefs: SharedPreferences by lazy {
|
||||
val masterKey =
|
||||
MasterKey.Builder(app).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||
EncryptedSharedPreferences.create(
|
||||
app,
|
||||
PREFS_FILE_NAME,
|
||||
masterKey,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
}
|
||||
|
||||
fun getPassphrase(deviceAddress: String): StoredPassphrase? {
|
||||
val key = sanitizeKey(deviceAddress)
|
||||
val passphrase = prefs.getString("${key}_passphrase", null) ?: return null
|
||||
val boots = prefs.getInt("${key}_boots", DEFAULT_BOOTS)
|
||||
val hours = prefs.getInt("${key}_hours", 0)
|
||||
return StoredPassphrase(passphrase, boots, hours)
|
||||
}
|
||||
|
||||
fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int) {
|
||||
val key = sanitizeKey(deviceAddress)
|
||||
prefs
|
||||
.edit()
|
||||
.putString("${key}_passphrase", passphrase)
|
||||
.putInt("${key}_boots", boots)
|
||||
.putInt("${key}_hours", hours)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun clearPassphrase(deviceAddress: String) {
|
||||
val key = sanitizeKey(deviceAddress)
|
||||
prefs.edit().remove("${key}_passphrase").remove("${key}_boots").remove("${key}_hours").apply()
|
||||
}
|
||||
|
||||
private fun sanitizeKey(address: String): String = address.replace(":", "_")
|
||||
|
||||
companion object {
|
||||
private const val PREFS_FILE_NAME = "lockdown_passphrase_store"
|
||||
const val DEFAULT_BOOTS = 50
|
||||
}
|
||||
}
|
||||
+30
@@ -26,6 +26,8 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.service.LockdownState
|
||||
import org.meshtastic.core.model.service.LockdownTokenInfo
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.model.service.TracerouteResponse
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
@@ -125,4 +127,32 @@ open class ServiceRepositoryImpl : ServiceRepository {
|
||||
override suspend fun onServiceAction(action: ServiceAction) {
|
||||
_serviceAction.send(action)
|
||||
}
|
||||
|
||||
private val _lockdownState = MutableStateFlow<LockdownState>(LockdownState.None)
|
||||
override val lockdownState: StateFlow<LockdownState>
|
||||
get() = _lockdownState
|
||||
|
||||
override fun setLockdownState(state: LockdownState) {
|
||||
_lockdownState.value = state
|
||||
}
|
||||
|
||||
override fun clearLockdownState() {
|
||||
_lockdownState.value = LockdownState.None
|
||||
}
|
||||
|
||||
private val _lockdownTokenInfo = MutableStateFlow<LockdownTokenInfo?>(null)
|
||||
override val lockdownTokenInfo: StateFlow<LockdownTokenInfo?>
|
||||
get() = _lockdownTokenInfo
|
||||
|
||||
override fun setLockdownTokenInfo(info: LockdownTokenInfo?) {
|
||||
_lockdownTokenInfo.value = info
|
||||
}
|
||||
|
||||
private val _sessionAuthorized = MutableStateFlow(false)
|
||||
override val sessionAuthorized: StateFlow<Boolean>
|
||||
get() = _sessionAuthorized
|
||||
|
||||
override fun setSessionAuthorized(authorized: Boolean) {
|
||||
_sessionAuthorized.value = authorized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ androidx-camera-viewfinder-compose = { module = "androidx.camera.viewfinder:view
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.18.0" }
|
||||
androidx-core-location-altitude = { module = "androidx.core:core-location-altitude", version = "1.0.0-rc01" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.2.0" }
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto", version = "1.1.0-alpha06" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||
androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
|
||||
|
||||
Reference in New Issue
Block a user