mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-03 14:06:34 +02:00
feat(lockdown): thread max_session_seconds through coordinator and UI
End-to-end plumbing for LockdownAuth.max_session_seconds (per-boot uptime cap on the unlocked session; 0 = unlimited). Wire: - CommandSenderImpl populates LockdownAuth.max_session_seconds in the outbound admin packet (clamped non-negative). Coordinator + persistence: - LockdownCoordinator.submitPassphrase gains optional maxSessionSeconds (default 0); persisted alongside boots/hours and replayed by auto-unlock so cached sessions keep the operator's cap on reconnect. - StoredPassphrase gains a new field with a default of 0 so existing call sites stay source-compatible. - LockdownPassphraseStore (Android EncryptedSharedPreferences impl): reads/writes the new field with a `_maxSessionSeconds` key suffix; legacy entries decode to 0. - LockdownPassphraseStore (JVM file-backed impl): bumps the per-entry on-disk serialization from 3-line to 4-line; legacy 3-line entries still decode (treated as maxSessionSeconds=0). IPC + radio plumbing: - IMeshService.sendLockdownUnlock AIDL gains a 4th int parameter. - MeshService stub, MeshActionHandler, RadioController interface, and both impls (AndroidRadioControllerImpl, DirectRadioControllerImpl) thread the field through. - FakeIMeshService, FakeRadioController, FakeLockdownCoordinator updated to match. UI: - LockdownDialog adds a single optional "Session cap (minutes)" field below the boots/hours row. Operators enter minutes for ergonomics; the dialog multiplies by 60 before passing to the coordinator. Blank or 0 = unlimited (firmware default). - UIViewModel.sendLockdownUnlock gains the new param with default 0. - New string resources: lockdown_session_minutes, lockdown_session_minutes_help. Strings re-sorted via scripts/sort-strings.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+2
@@ -599,6 +599,8 @@ lockdown_passphrase
|
||||
lockdown_passphrases_do_not_match
|
||||
lockdown_session_boots_remaining
|
||||
lockdown_session_expires
|
||||
lockdown_session_minutes
|
||||
lockdown_session_minutes_help
|
||||
lockdown_session_no_time_limit
|
||||
lockdown_set_passphrase
|
||||
lockdown_show_passphrase
|
||||
|
||||
@@ -72,7 +72,9 @@ fun MainScreen() {
|
||||
val lockdownState by viewModel.lockdownState.collectAsStateWithLifecycle()
|
||||
LockdownDialog(
|
||||
lockdownState = lockdownState,
|
||||
onSubmit = { passphrase, boots, hours -> viewModel.sendLockdownUnlock(passphrase, boots, hours) },
|
||||
onSubmit = { passphrase, boots, hours, sessionMinutes ->
|
||||
viewModel.sendLockdownUnlock(passphrase, boots, hours, sessionMinutes * SECONDS_PER_MINUTE)
|
||||
},
|
||||
onDisconnect = { viewModel.setDeviceAddress("n") },
|
||||
)
|
||||
// Auto-disconnect when firmware acknowledges Lock Now
|
||||
@@ -145,3 +147,5 @@ private fun AndroidAppVersionCheck(viewModel: UIViewModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val SECONDS_PER_MINUTE = 60
|
||||
|
||||
@@ -206,7 +206,7 @@ interface IMeshService {
|
||||
void requestRebootOta(in int requestId, in int destNum, in int mode, in byte []hash);
|
||||
|
||||
/// Send a lockdown passphrase to authenticate with a TAK-locked device
|
||||
void sendLockdownUnlock(in String passphrase, in int bootTtl, in int hourTtl);
|
||||
void sendLockdownUnlock(in String passphrase, in int bootTtl, in int hourTtl, in int maxSessionSeconds);
|
||||
|
||||
/// Send a Lock Now command to the connected TAK-enabled device
|
||||
void sendLockNow();
|
||||
|
||||
+2
-1
@@ -375,7 +375,7 @@ class CommandSenderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendLockdownPassphrase(passphrase: String, boots: Int, hours: Int) {
|
||||
override fun sendLockdownPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int) {
|
||||
val validUntilEpoch =
|
||||
if (hours > 0) {
|
||||
(nowMillis / MILLIS_PER_SECOND + hours.toLong() * SECONDS_PER_HOUR).toInt()
|
||||
@@ -387,6 +387,7 @@ class CommandSenderImpl(
|
||||
passphrase = passphrase.encodeToByteArray().toByteString(),
|
||||
boots_remaining = boots.coerceAtLeast(0),
|
||||
valid_until_epoch = validUntilEpoch,
|
||||
max_session_seconds = maxSessionSeconds.coerceAtLeast(0),
|
||||
)
|
||||
sendLockdownAdmin(AdminMessage(lockdown_auth = lockdownAuth))
|
||||
}
|
||||
|
||||
+19
-4
@@ -56,6 +56,8 @@ class LockdownCoordinatorImpl(
|
||||
|
||||
@Volatile private var pendingHours: Int = 0
|
||||
|
||||
@Volatile private var pendingMaxSessionSeconds: Int = 0
|
||||
|
||||
override fun onConnect() {
|
||||
serviceRepository.setSessionAuthorized(false)
|
||||
resetTransientState()
|
||||
@@ -108,7 +110,12 @@ class LockdownCoordinatorImpl(
|
||||
if (stored != null) {
|
||||
Logger.i { "Lockdown: Auto-unlocking with stored passphrase" }
|
||||
wasAutoAttempt = true
|
||||
commandSender.sendLockdownPassphrase(stored.passphrase, stored.boots, stored.hours)
|
||||
commandSender.sendLockdownPassphrase(
|
||||
stored.passphrase,
|
||||
stored.boots,
|
||||
stored.hours,
|
||||
stored.maxSessionSeconds,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -126,7 +133,13 @@ class LockdownCoordinatorImpl(
|
||||
// Only save on manual submit — auto-unlock already has a stored passphrase.
|
||||
if (deviceAddress != null && passphrase != null) {
|
||||
try {
|
||||
passphraseStore.savePassphrase(deviceAddress, passphrase, pendingBoots, pendingHours)
|
||||
passphraseStore.savePassphrase(
|
||||
deviceAddress,
|
||||
passphrase,
|
||||
pendingBoots,
|
||||
pendingHours,
|
||||
pendingMaxSessionSeconds,
|
||||
)
|
||||
Logger.i { "Lockdown: Saved passphrase for device" }
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Lockdown: Failed to persist passphrase (session still unlocked)" }
|
||||
@@ -174,14 +187,15 @@ class LockdownCoordinatorImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun submitPassphrase(passphrase: String, boots: Int, hours: Int) {
|
||||
override fun submitPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int) {
|
||||
pendingPassphrase = passphrase
|
||||
pendingBoots = boots
|
||||
pendingHours = hours
|
||||
pendingMaxSessionSeconds = maxSessionSeconds
|
||||
wasAutoAttempt = false
|
||||
wasLockNow = false
|
||||
serviceRepository.setLockdownState(LockdownState.None)
|
||||
commandSender.sendLockdownPassphrase(passphrase, boots, hours)
|
||||
commandSender.sendLockdownPassphrase(passphrase, boots, hours, maxSessionSeconds)
|
||||
}
|
||||
|
||||
override fun lockNow() {
|
||||
@@ -195,5 +209,6 @@ class LockdownCoordinatorImpl(
|
||||
pendingPassphrase = null
|
||||
pendingBoots = LockdownPassphraseStore.DEFAULT_BOOTS
|
||||
pendingHours = 0
|
||||
pendingMaxSessionSeconds = 0
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -404,8 +404,8 @@ class MeshActionHandlerImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
|
||||
lockdownCoordinator.submitPassphrase(passphrase, bootTtl, hourTtl)
|
||||
override fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {
|
||||
lockdownCoordinator.submitPassphrase(passphrase, bootTtl, hourTtl, maxSessionSeconds)
|
||||
}
|
||||
|
||||
override fun handleSendLockNow() {
|
||||
|
||||
@@ -341,7 +341,7 @@ interface RadioController {
|
||||
fun setDeviceAddress(address: String)
|
||||
|
||||
/** Submits a lockdown passphrase to authenticate with a TAK-locked device. */
|
||||
suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int)
|
||||
suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int = 0)
|
||||
|
||||
/** Sends a Lock Now command to the connected TAK-enabled device. */
|
||||
suspend fun sendLockNow()
|
||||
|
||||
+6
-1
@@ -85,7 +85,12 @@ interface CommandSender {
|
||||
fun requestNeighborInfo(requestId: Int, destNum: Int)
|
||||
|
||||
/** Sends a lockdown passphrase to authenticate with a TAK-locked device. */
|
||||
fun sendLockdownPassphrase(passphrase: String, boots: Int = 0, hours: Int = 0)
|
||||
fun sendLockdownPassphrase(
|
||||
passphrase: String,
|
||||
boots: Int = 0,
|
||||
hours: Int = 0,
|
||||
maxSessionSeconds: Int = 0,
|
||||
)
|
||||
|
||||
/** Sends a Lock Now command to immediately lock a TAK-enabled device. */
|
||||
fun sendLockNow()
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ interface LockdownCoordinator {
|
||||
fun handleLockdownStatus(status: LockdownStatus)
|
||||
|
||||
/** Submits a passphrase to authenticate with the locked device. */
|
||||
fun submitPassphrase(passphrase: String, boots: Int, hours: Int)
|
||||
fun submitPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int = 0)
|
||||
|
||||
/** Sends a Lock Now command to the connected device. */
|
||||
fun lockNow()
|
||||
|
||||
+20
-3
@@ -16,8 +16,19 @@
|
||||
*/
|
||||
package org.meshtastic.core.repository
|
||||
|
||||
/** Stored passphrase entry with associated TTL parameters. */
|
||||
data class StoredPassphrase(val passphrase: String, val boots: Int, val hours: Int) {
|
||||
/**
|
||||
* Stored passphrase entry with associated TTL parameters.
|
||||
*
|
||||
* @param maxSessionSeconds Per-boot uptime cap, in seconds. 0 = unlimited.
|
||||
* Non-zero is firmware-side enforcement: the device revokes auth and reboots
|
||||
* after this many seconds of uptime even if the boot-count TTL is still valid.
|
||||
*/
|
||||
data class StoredPassphrase(
|
||||
val passphrase: String,
|
||||
val boots: Int,
|
||||
val hours: Int,
|
||||
val maxSessionSeconds: Int = 0,
|
||||
) {
|
||||
init {
|
||||
require(passphrase.isNotEmpty()) { "passphrase must not be empty" }
|
||||
}
|
||||
@@ -35,7 +46,13 @@ interface LockdownPassphraseStore {
|
||||
fun getPassphrase(deviceAddress: String): StoredPassphrase?
|
||||
|
||||
/** Saves the passphrase and TTL parameters for the given device address. */
|
||||
fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int)
|
||||
fun savePassphrase(
|
||||
deviceAddress: String,
|
||||
passphrase: String,
|
||||
boots: Int,
|
||||
hours: Int,
|
||||
maxSessionSeconds: Int = 0,
|
||||
)
|
||||
|
||||
/** Clears the stored passphrase for the given device address. */
|
||||
fun clearPassphrase(deviceAddress: String)
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ interface MeshActionHandler {
|
||||
fun handleUpdateLastAddress(deviceAddr: String?)
|
||||
|
||||
/** Submits a lockdown passphrase to authenticate with a TAK-locked device. */
|
||||
fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int)
|
||||
fun handleSendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int = 0)
|
||||
|
||||
/** Sends a Lock Now command to the connected TAK-enabled device. */
|
||||
fun handleSendLockNow()
|
||||
|
||||
@@ -623,6 +623,8 @@
|
||||
<string name="lockdown_passphrases_do_not_match">Passphrases do not match</string>
|
||||
<string name="lockdown_session_boots_remaining">Session: %1$d reboots remaining</string>
|
||||
<string name="lockdown_session_expires">Expires %1$s</string>
|
||||
<string name="lockdown_session_minutes">Session cap (minutes)</string>
|
||||
<string name="lockdown_session_minutes_help">Per-boot uptime cap. 0 = unlimited.</string>
|
||||
<string name="lockdown_session_no_time_limit">No time limit</string>
|
||||
<string name="lockdown_set_passphrase">Set Passphrase</string>
|
||||
<string name="lockdown_show_passphrase">Show</string>
|
||||
|
||||
+2
-2
@@ -221,8 +221,8 @@ class AndroidRadioControllerImpl(
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
|
||||
serviceRepository.meshService?.sendLockdownUnlock(passphrase, bootTtl, hourTtl)
|
||||
override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {
|
||||
serviceRepository.meshService?.sendLockdownUnlock(passphrase, bootTtl, hourTtl, maxSessionSeconds)
|
||||
}
|
||||
|
||||
override suspend fun sendLockNow() {
|
||||
|
||||
+16
-3
@@ -61,23 +61,36 @@ class LockdownPassphraseStoreImpl(app: Application) : LockdownPassphraseStore {
|
||||
val passphrase = p.getString("${key}_passphrase", null) ?: return null
|
||||
val boots = p.getInt("${key}_boots", LockdownPassphraseStore.DEFAULT_BOOTS)
|
||||
val hours = p.getInt("${key}_hours", 0)
|
||||
return StoredPassphrase(passphrase, boots, hours)
|
||||
val maxSessionSeconds = p.getInt("${key}_maxSessionSeconds", 0)
|
||||
return StoredPassphrase(passphrase, boots, hours, maxSessionSeconds)
|
||||
}
|
||||
|
||||
override fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int) {
|
||||
override fun savePassphrase(
|
||||
deviceAddress: String,
|
||||
passphrase: String,
|
||||
boots: Int,
|
||||
hours: Int,
|
||||
maxSessionSeconds: Int,
|
||||
) {
|
||||
val p = requirePrefs()
|
||||
val key = sanitizeKey(deviceAddress)
|
||||
p.edit()
|
||||
.putString("${key}_passphrase", passphrase)
|
||||
.putInt("${key}_boots", boots)
|
||||
.putInt("${key}_hours", hours)
|
||||
.putInt("${key}_maxSessionSeconds", maxSessionSeconds)
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun clearPassphrase(deviceAddress: String) {
|
||||
val p = requirePrefs()
|
||||
val key = sanitizeKey(deviceAddress)
|
||||
p.edit().remove("${key}_passphrase").remove("${key}_boots").remove("${key}_hours").apply()
|
||||
p.edit()
|
||||
.remove("${key}_passphrase")
|
||||
.remove("${key}_boots")
|
||||
.remove("${key}_hours")
|
||||
.remove("${key}_maxSessionSeconds")
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun sanitizeKey(address: String): String = address.replace(":", "_")
|
||||
|
||||
@@ -402,8 +402,18 @@ class MeshService : Service() {
|
||||
router.actionHandler.handleRequestRebootOta(requestId, destNum, mode, hash)
|
||||
}
|
||||
|
||||
override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) = toRemoteExceptions {
|
||||
router.actionHandler.handleSendLockdownUnlock(passphrase.orEmpty(), bootTtl, hourTtl)
|
||||
override fun sendLockdownUnlock(
|
||||
passphrase: String?,
|
||||
bootTtl: Int,
|
||||
hourTtl: Int,
|
||||
maxSessionSeconds: Int,
|
||||
) = toRemoteExceptions {
|
||||
router.actionHandler.handleSendLockdownUnlock(
|
||||
passphrase.orEmpty(),
|
||||
bootTtl,
|
||||
hourTtl,
|
||||
maxSessionSeconds,
|
||||
)
|
||||
}
|
||||
|
||||
override fun sendLockNow() = toRemoteExceptions { router.actionHandler.handleSendLockNow() }
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ open class FakeIMeshService : IMeshService.Stub() {
|
||||
|
||||
override fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
|
||||
|
||||
override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int) {}
|
||||
override fun sendLockdownUnlock(passphrase: String?, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {}
|
||||
|
||||
override fun sendLockNow() {}
|
||||
}
|
||||
|
||||
+2
-2
@@ -235,8 +235,8 @@ class DirectRadioControllerImpl(
|
||||
radioInterfaceService.setDeviceAddress(address)
|
||||
}
|
||||
|
||||
override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {
|
||||
actionHandler.handleSendLockdownUnlock(passphrase, bootTtl, hourTtl)
|
||||
override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {
|
||||
actionHandler.handleSendLockdownUnlock(passphrase, bootTtl, hourTtl, maxSessionSeconds)
|
||||
}
|
||||
|
||||
override suspend fun sendLockNow() {
|
||||
|
||||
+35
-10
@@ -71,9 +71,15 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore {
|
||||
}
|
||||
}
|
||||
|
||||
override fun savePassphrase(deviceAddress: String, passphrase: String, boots: Int, hours: Int) {
|
||||
override fun savePassphrase(
|
||||
deviceAddress: String,
|
||||
passphrase: String,
|
||||
boots: Int,
|
||||
hours: Int,
|
||||
maxSessionSeconds: Int,
|
||||
) {
|
||||
val key = masterKey ?: error("Lockdown: Cannot save passphrase - keystore unavailable")
|
||||
val plaintext = serialize(passphrase, boots, hours)
|
||||
val plaintext = serialize(passphrase, boots, hours, maxSessionSeconds)
|
||||
val encrypted = encrypt(key, plaintext)
|
||||
entryFile(deviceAddress).writeBytes(encrypted)
|
||||
}
|
||||
@@ -114,24 +120,42 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore {
|
||||
|
||||
// region Serialization (simple line-based to avoid adding kotlinx-serialization dependency)
|
||||
|
||||
private fun serialize(passphrase: String, boots: Int, hours: Int): ByteArray =
|
||||
"$boots\n$hours\n$passphrase".encodeToByteArray()
|
||||
// Format v2: "boots\nhours\nmaxSessionSeconds\npassphrase" (4 lines).
|
||||
// Backward-compat: legacy 3-line entries (no maxSessionSeconds) decode with maxSessionSeconds=0.
|
||||
private fun serialize(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int): ByteArray =
|
||||
"$boots\n$hours\n$maxSessionSeconds\n$passphrase".encodeToByteArray()
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private fun deserialize(plaintext: ByteArray): StoredPassphrase? {
|
||||
val text = plaintext.decodeToString()
|
||||
val lines = text.split("\n", limit = 3)
|
||||
if (lines.size < SERIALIZED_LINE_COUNT) {
|
||||
// Try v2 (4-line) format first.
|
||||
val v2 = text.split("\n", limit = 4)
|
||||
if (v2.size == SERIALIZED_LINE_COUNT_V2) {
|
||||
val boots = v2[0].toIntOrNull()
|
||||
val hours = v2[1].toIntOrNull()
|
||||
val maxSession = v2[2].toIntOrNull()
|
||||
if (boots != null && hours != null && maxSession != null) {
|
||||
return StoredPassphrase(
|
||||
passphrase = v2[3],
|
||||
boots = boots,
|
||||
hours = hours,
|
||||
maxSessionSeconds = maxSession,
|
||||
)
|
||||
}
|
||||
}
|
||||
// Fall back to v1 (3-line, no maxSessionSeconds).
|
||||
val v1 = text.split("\n", limit = 3)
|
||||
if (v1.size < SERIALIZED_LINE_COUNT_V1) {
|
||||
Logger.w { "Lockdown: Invalid passphrase entry format" }
|
||||
return null
|
||||
}
|
||||
val boots = lines[0].toIntOrNull()
|
||||
val hours = lines[1].toIntOrNull()
|
||||
val boots = v1[0].toIntOrNull()
|
||||
val hours = v1[1].toIntOrNull()
|
||||
if (boots == null || hours == null) {
|
||||
Logger.w { "Lockdown: Invalid passphrase entry metadata" }
|
||||
return null
|
||||
}
|
||||
return StoredPassphrase(passphrase = lines[2], boots = boots, hours = hours)
|
||||
return StoredPassphrase(passphrase = v1[2], boots = boots, hours = hours)
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -172,6 +196,7 @@ class LockdownPassphraseStoreImpl : LockdownPassphraseStore {
|
||||
private const val AES_KEY_BITS = 256
|
||||
private const val GCM_TAG_BITS = 128
|
||||
private const val BYTE_MASK = 0xFF
|
||||
private const val SERIALIZED_LINE_COUNT = 3
|
||||
private const val SERIALIZED_LINE_COUNT_V1 = 3
|
||||
private const val SERIALIZED_LINE_COUNT_V2 = 4
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -27,6 +27,7 @@ class FakeLockdownCoordinator : LockdownCoordinator {
|
||||
var lastPassphrase: String? = null
|
||||
var lastBoots: Int? = null
|
||||
var lastHours: Int? = null
|
||||
var lastMaxSessionSeconds: Int? = null
|
||||
var lockNowCalled = false
|
||||
|
||||
override fun onConnect() {
|
||||
@@ -45,10 +46,11 @@ class FakeLockdownCoordinator : LockdownCoordinator {
|
||||
lastStatus = status
|
||||
}
|
||||
|
||||
override fun submitPassphrase(passphrase: String, boots: Int, hours: Int) {
|
||||
override fun submitPassphrase(passphrase: String, boots: Int, hours: Int, maxSessionSeconds: Int) {
|
||||
lastPassphrase = passphrase
|
||||
lastBoots = boots
|
||||
lastHours = hours
|
||||
lastMaxSessionSeconds = maxSessionSeconds
|
||||
}
|
||||
|
||||
override fun lockNow() {
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ class FakeRadioController :
|
||||
lastSetDeviceAddress = address
|
||||
}
|
||||
|
||||
override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int) {}
|
||||
override suspend fun sendLockdownUnlock(passphrase: String, bootTtl: Int, hourTtl: Int, maxSessionSeconds: Int) {}
|
||||
|
||||
override suspend fun sendLockNow() {}
|
||||
|
||||
|
||||
@@ -141,8 +141,13 @@ class UIViewModel(
|
||||
val lockdownState = serviceRepository.lockdownState
|
||||
val lockdownTokenInfo = serviceRepository.lockdownTokenInfo
|
||||
|
||||
fun sendLockdownUnlock(passphrase: String, bootTtl: Int = DEFAULT_BOOT_TTL, hourTtl: Int = 0) {
|
||||
viewModelScope.launch { radioController.sendLockdownUnlock(passphrase, bootTtl, hourTtl) }
|
||||
fun sendLockdownUnlock(
|
||||
passphrase: String,
|
||||
bootTtl: Int = DEFAULT_BOOT_TTL,
|
||||
hourTtl: Int = 0,
|
||||
maxSessionSeconds: Int = 0,
|
||||
) {
|
||||
viewModelScope.launch { radioController.sendLockdownUnlock(passphrase, bootTtl, hourTtl, maxSessionSeconds) }
|
||||
}
|
||||
|
||||
fun sendLockNow() {
|
||||
|
||||
+15
-2
@@ -57,6 +57,8 @@ import org.meshtastic.core.resources.lockdown_incorrect_passphrase
|
||||
import org.meshtastic.core.resources.lockdown_lock_reason
|
||||
import org.meshtastic.core.resources.lockdown_passphrase
|
||||
import org.meshtastic.core.resources.lockdown_passphrases_do_not_match
|
||||
import org.meshtastic.core.resources.lockdown_session_minutes
|
||||
import org.meshtastic.core.resources.lockdown_session_minutes_help
|
||||
import org.meshtastic.core.resources.lockdown_set_passphrase
|
||||
import org.meshtastic.core.resources.lockdown_show_passphrase
|
||||
import org.meshtastic.core.resources.lockdown_submit
|
||||
@@ -75,7 +77,7 @@ import org.meshtastic.core.ui.icon.VisibilityOff
|
||||
@Composable
|
||||
fun LockdownDialog(
|
||||
lockdownState: LockdownState,
|
||||
onSubmit: (passphrase: String, boots: Int, hours: Int) -> Unit,
|
||||
onSubmit: (passphrase: String, boots: Int, hours: Int, sessionMinutes: Int) -> Unit,
|
||||
onDisconnect: () -> Unit,
|
||||
) {
|
||||
val shouldShow =
|
||||
@@ -93,6 +95,7 @@ fun LockdownDialog(
|
||||
var passwordVisible by rememberSaveable { mutableStateOf(false) }
|
||||
var boots by rememberSaveable { mutableIntStateOf(LockdownPassphraseStore.DEFAULT_BOOTS) }
|
||||
var hours by rememberSaveable { mutableIntStateOf(0) }
|
||||
var sessionMinutes by rememberSaveable { mutableIntStateOf(0) }
|
||||
|
||||
val isProvisioning = lockdownState is LockdownState.NeedsProvision
|
||||
val title =
|
||||
@@ -208,10 +211,20 @@ fun LockdownDialog(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(SPACING_DP.dp))
|
||||
OutlinedTextField(
|
||||
value = sessionMinutes.toString(),
|
||||
onValueChange = { str -> str.toIntOrNull()?.let { sessionMinutes = it.coerceAtLeast(0) } },
|
||||
label = { Text(stringResource(Res.string.lockdown_session_minutes)) },
|
||||
supportingText = { Text(stringResource(Res.string.lockdown_session_minutes_help)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSubmit(passphrase, boots, hours) }, enabled = isValid) {
|
||||
TextButton(onClick = { onSubmit(passphrase, boots, hours, sessionMinutes) }, enabled = isValid) {
|
||||
Text(stringResource(Res.string.lockdown_submit))
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user