修正应用锁逻辑错误

This commit is contained in:
2026-06-17 09:53:56 +08:00
parent 4ebd1babe3
commit be613749c4
10 changed files with 227 additions and 124 deletions
@@ -61,6 +61,10 @@ class MainActivity : FragmentActivity() {
val isFlagSecureEnabled by settingsRepo.isFlagSecureEnabled.collectAsStateWithLifecycle(initialValue = false) val isFlagSecureEnabled by settingsRepo.isFlagSecureEnabled.collectAsStateWithLifecycle(initialValue = false)
val isLocked by appLockManager.isLocked.collectAsStateWithLifecycle() val isLocked by appLockManager.isLocked.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
appLockManager.checkInitialLockState()
}
LaunchedEffect(isFlagSecureEnabled) { LaunchedEffect(isFlagSecureEnabled) {
if (isFlagSecureEnabled) { if (isFlagSecureEnabled) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
@@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -30,40 +29,84 @@ class AppLockManager(
val isLocked: StateFlow<Boolean> = _isLocked.asStateFlow() val isLocked: StateFlow<Boolean> = _isLocked.asStateFlow()
private var pendingLockJob: Job? = null private var pendingLockJob: Job? = null
private var lockDeadlineAtMillis: Long? = null
private val screenOffReceiver = object : BroadcastReceiver() { private val screenOffReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_SCREEN_OFF) { if (intent?.action == Intent.ACTION_SCREEN_OFF) {
scheduleLock() recordBackgroundAndScheduleLock()
}
}
}
private val screenOnReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_SCREEN_ON) {
cancelPendingLockAndClearBackgroundTime()
} }
} }
} }
init { init {
appContext.registerReceiver(screenOffReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF)) appContext.registerReceiver(screenOffReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
appContext.registerReceiver(screenOnReceiver, IntentFilter(Intent.ACTION_SCREEN_ON))
observeSettings() observeSettings()
} }
/**
* Called when the app first starts or is restored after process death.
* Checks the persisted last-background time against the lock trigger delay
* to decide whether the app should be locked.
*/
fun checkInitialLockState() {
if (_isLocked.value) return
scope.launch {
val settings = settingsRepository.appLockSettings.first()
if (!settings.isAppLockEnabled) return@launch
val lastBg = settingsRepository.getLastBackgroundTime()
if (lastBg <= 0L) {
// No recorded background time (fresh launch after PIN enabled)
_isLocked.value = true
return@launch
}
val delayMillis = settings.lockTrigger.delayMillis
val now = System.currentTimeMillis()
val shouldLock = if (delayMillis <= 0L) {
// Immediate: any recorded background time means lock
true
} else {
// Delayed: lock if enough time has passed since background
now >= lastBg + delayMillis
}
if (shouldLock) {
_isLocked.value = true
}
}
}
fun lock() { fun lock() {
pendingLockJob?.cancel() pendingLockJob?.cancel()
pendingLockJob = null pendingLockJob = null
lockDeadlineAtMillis = null
scope.launch { scope.launch {
val appLockSettings = settingsRepository.appLockSettings.first() val settings = settingsRepository.appLockSettings.first()
_isLocked.value = appLockSettings.isAppLockEnabled _isLocked.value = settings.isAppLockEnabled
} }
} }
fun unlock() { fun unlock() {
pendingLockJob?.cancel() pendingLockJob?.cancel()
pendingLockJob = null pendingLockJob = null
lockDeadlineAtMillis = null scope.launch {
settingsRepository.clearLastBackgroundTime()
}
_isLocked.value = false _isLocked.value = false
} }
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
super.onStart(owner) super.onStart(owner)
cancelPendingLock()
scope.launch { scope.launch {
refreshLockStateWhenForegrounded() refreshLockStateWhenForegrounded()
} }
@@ -71,7 +114,7 @@ class AppLockManager(
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
super.onStop(owner) super.onStop(owner)
scheduleLock() recordBackgroundAndScheduleLock()
} }
private fun observeSettings() { private fun observeSettings() {
@@ -80,65 +123,89 @@ class AppLockManager(
if (!settings.isAppLockEnabled) { if (!settings.isAppLockEnabled) {
pendingLockJob?.cancel() pendingLockJob?.cancel()
pendingLockJob = null pendingLockJob = null
lockDeadlineAtMillis = null
_isLocked.value = false _isLocked.value = false
scope.launch { settingsRepository.clearLastBackgroundTime() }
} }
} }
} }
} }
private fun scheduleLock() { /**
* Persist the current time as the background moment, then schedule
* an in-memory lock after the configured delay.
*/
private fun recordBackgroundAndScheduleLock() {
if (_isLocked.value) return
pendingLockJob?.cancel() pendingLockJob?.cancel()
pendingLockJob = null pendingLockJob = null
scope.launch { scope.launch {
val appLockSettings = settingsRepository.appLockSettings.first() val settings = settingsRepository.appLockSettings.first()
if (!appLockSettings.isAppLockEnabled) { if (!settings.isAppLockEnabled) {
lockDeadlineAtMillis = null
_isLocked.value = false _isLocked.value = false
return@launch return@launch
} }
val delayMillis = appLockSettings.lockTrigger.delayMillis // Persist the background timestamp so a process-kill doesn't lose it
settingsRepository.setLastBackgroundTime(System.currentTimeMillis())
val delayMillis = settings.lockTrigger.delayMillis
if (delayMillis <= 0L) { if (delayMillis <= 0L) {
lockDeadlineAtMillis = null
_isLocked.value = true _isLocked.value = true
return@launch return@launch
} }
val deadline = System.currentTimeMillis() + delayMillis
lockDeadlineAtMillis = deadline
pendingLockJob = scope.launch { pendingLockJob = scope.launch {
delay(delayMillis) delay(delayMillis)
val latestSettings = settingsRepository.appLockSettings.first() val latestSettings = settingsRepository.appLockSettings.first()
if (latestSettings.isAppLockEnabled && lockDeadlineAtMillis == deadline) { if (latestSettings.isAppLockEnabled) {
_isLocked.value = true _isLocked.value = true
} }
} }
} }
} }
private fun cancelPendingLock() {
pendingLockJob?.cancel()
pendingLockJob = null
}
private fun cancelPendingLockAndClearBackgroundTime() {
if (_isLocked.value) return // Already locked, don't interfere
pendingLockJob?.cancel()
pendingLockJob = null
scope.launch {
settingsRepository.clearLastBackgroundTime()
}
}
private suspend fun refreshLockStateWhenForegrounded() { private suspend fun refreshLockStateWhenForegrounded() {
val appLockSettings = settingsRepository.appLockSettings.first() val settings = settingsRepository.appLockSettings.first()
if (!appLockSettings.isAppLockEnabled) { if (!settings.isAppLockEnabled) {
unlock() unlock()
return return
} }
val deadline = lockDeadlineAtMillis val lastBg = settingsRepository.getLastBackgroundTime()
if (deadline == null) { if (lastBg <= 0L) return
return
val delayMillis = settings.lockTrigger.delayMillis
val now = System.currentTimeMillis()
val shouldLock = if (delayMillis <= 0L) {
true
} else {
now >= lastBg + delayMillis
} }
if (System.currentTimeMillis() >= deadline) { if (shouldLock) {
pendingLockJob?.cancel() cancelPendingLock()
pendingLockJob = null
lockDeadlineAtMillis = null
_isLocked.value = true _isLocked.value = true
} else { } else {
pendingLockJob?.cancel() // Still within the grace period — cancel the pending in-memory lock
pendingLockJob = null cancelPendingLock()
lockDeadlineAtMillis = null settingsRepository.clearLastBackgroundTime()
} }
} }
} }
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@@ -162,6 +163,22 @@ class SettingsRepository(private val context: Context, private val cryptoManager
} }
} }
suspend fun getLastBackgroundTime(): Long {
return dataStore.data.map { it[PreferencesKeys.LAST_BACKGROUND_TIME] ?: 0L }.first()
}
suspend fun setLastBackgroundTime(time: Long) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.LAST_BACKGROUND_TIME] = time
}
}
suspend fun clearLastBackgroundTime() {
dataStore.edit { preferences ->
preferences.remove(PreferencesKeys.LAST_BACKGROUND_TIME)
}
}
val isFlagSecureEnabled: Flow<Boolean> = dataStore.data.map { preferences -> val isFlagSecureEnabled: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[PreferencesKeys.FLAG_SECURE_ENABLED] ?: false preferences[PreferencesKeys.FLAG_SECURE_ENABLED] ?: false
} }
@@ -244,6 +261,7 @@ class SettingsRepository(private val context: Context, private val cryptoManager
val FAILED_ATTEMPTS = intPreferencesKey("failed_attempts") val FAILED_ATTEMPTS = intPreferencesKey("failed_attempts")
val LOCKOUT_END_TIME = longPreferencesKey("lockout_end_time") val LOCKOUT_END_TIME = longPreferencesKey("lockout_end_time")
val APP_LOCK_TRIGGER = intPreferencesKey("app_lock_trigger") val APP_LOCK_TRIGGER = intPreferencesKey("app_lock_trigger")
val LAST_BACKGROUND_TIME = longPreferencesKey("last_background_time")
val THEME_MODE = intPreferencesKey("theme_mode") val THEME_MODE = intPreferencesKey("theme_mode")
val DYNAMIC_COLOR = booleanPreferencesKey("dynamic_color") val DYNAMIC_COLOR = booleanPreferencesKey("dynamic_color")
val SEED_COLOR = intPreferencesKey("seed_color") val SEED_COLOR = intPreferencesKey("seed_color")
@@ -1,5 +1,7 @@
package cn.airnan.a2fair.ui.settings.security package cn.airnan.a2fair.ui.settings.security
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -15,9 +17,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -45,12 +50,23 @@ fun PinIndicatorRow(
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
repeat(PIN_LENGTH) { index -> repeat(PIN_LENGTH) { index ->
val isFilled = index < filledCount
val scale = remember { Animatable(1f) }
LaunchedEffect(isFilled) {
if (isFilled) {
scale.snapTo(1.4f)
scale.animateTo(1f, spring(dampingRatio = 0.35f, stiffness = 600f))
}
}
Box( Box(
modifier = Modifier modifier = Modifier
.padding(horizontal = 6.dp) .padding(horizontal = 6.dp)
.size(14.dp) .size(14.dp)
.scale(scale.value)
.clip(CircleShape) .clip(CircleShape)
.background(if (index < filledCount) activeColor else inactiveColor) .background(if (isFilled) activeColor else inactiveColor)
) )
} }
} }
@@ -88,10 +104,10 @@ fun PinNumberPad(
onClick = { onDigit(digit) }, onClick = { onDigit(digit) },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(56.dp), .height(72.dp),
enabled = isEnabled enabled = isEnabled
) { ) {
Text(text = digit, style = MaterialTheme.typography.titleLarge) Text(text = digit, style = MaterialTheme.typography.headlineMedium)
} }
} }
} }
@@ -106,7 +122,7 @@ fun PinNumberPad(
onClick = onReset, onClick = onReset,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(56.dp), .height(72.dp),
enabled = isEnabled && isResetEnabled enabled = isEnabled && isResetEnabled
) { ) {
Text(text = resetLabel) Text(text = resetLabel)
@@ -116,7 +132,7 @@ fun PinNumberPad(
onClick = { onDigit("0") }, onClick = { onDigit("0") },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(56.dp), .height(72.dp),
enabled = isEnabled enabled = isEnabled
) { ) {
Text(text = "0", style = MaterialTheme.typography.titleLarge) Text(text = "0", style = MaterialTheme.typography.titleLarge)
@@ -126,7 +142,7 @@ fun PinNumberPad(
onClick = onDelete, onClick = onDelete,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(56.dp) .height(72.dp)
.semantics { contentDescription = deleteLabel }, .semantics { contentDescription = deleteLabel },
enabled = isEnabled && isDeleteEnabled enabled = isEnabled && isDeleteEnabled
) { ) {
@@ -1,6 +1,7 @@
package cn.airnan.a2fair.ui.settings.security package cn.airnan.a2fair.ui.settings.security
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -38,7 +39,7 @@ fun PinSetupScreen(
R.string.pin_digits_remaining, R.string.pin_digits_remaining,
PIN_LENGTH - currentPin.length PIN_LENGTH - currentPin.length
) )
else -> pinRequirementMsg else -> ""
} }
Scaffold( Scaffold(
@@ -68,13 +69,6 @@ fun PinSetupScreen(
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text(
text = pinRequirementMsg,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
PinIndicatorRow( PinIndicatorRow(
filledCount = currentPin.length, filledCount = currentPin.length,
isError = errorMessage != null isError = errorMessage != null
@@ -90,7 +84,7 @@ fun PinSetupScreen(
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
} }
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(36.dp))
PinNumberPad( PinNumberPad(
resetLabel = if (step == 1) { resetLabel = if (step == 1) {
@@ -131,33 +125,44 @@ fun PinSetupScreen(
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Button( Row(
onClick = { modifier = Modifier.fillMaxWidth(),
if (step == 1) { horizontalArrangement = Arrangement.End
if (pin.length != PIN_LENGTH) { ) {
errorMessage = pinTooShortMsg Button(
} else { onClick = {
confirmPin = "" if (step == 1) {
errorMessage = null if (pin.length != PIN_LENGTH) {
step = 2 errorMessage = pinTooShortMsg
} } else {
} else { confirmPin = ""
if (confirmPin.length != PIN_LENGTH) { errorMessage = null
errorMessage = pinTooShortMsg step = 2
} else if (pin == confirmPin) {
scope.launch {
settingsRepository.enablePin(pin)
onNavigateBack()
} }
} else { } else {
errorMessage = pinsDoNotMatchMsg if (confirmPin.length != PIN_LENGTH) {
confirmPin = "" errorMessage = pinTooShortMsg
} else if (pin == confirmPin) {
scope.launch {
settingsRepository.enablePin(pin)
onNavigateBack()
}
} else {
errorMessage = pinsDoNotMatchMsg
confirmPin = ""
}
} }
} },
}, modifier = Modifier
modifier = Modifier.fillMaxWidth() .height(56.dp)
) { .widthIn(min = 160.dp),
Text(if (step == 1) stringResource(R.string.next) else stringResource(R.string.confirm)) shape = RoundedCornerShape(16.dp)
) {
Text(
text = if (step == 1) stringResource(R.string.next) else stringResource(R.string.confirm),
style = MaterialTheme.typography.titleMedium
)
}
} }
} }
} }
@@ -43,9 +43,7 @@ fun SecuritySettingsScreen(
val enableBiometricTitle = stringResource(R.string.enable_biometric_verification_title) val enableBiometricTitle = stringResource(R.string.enable_biometric_verification_title)
val enableBiometricSubtitle = stringResource(R.string.enable_biometric_verification_desc) val enableBiometricSubtitle = stringResource(R.string.enable_biometric_verification_desc)
val disableBiometricTitle = stringResource(R.string.disable_biometric_verification_title) val disableBiometricTitle = stringResource(R.string.disable_biometric_verification_title)
val disableBiometricDesc = stringResource(R.string.disable_biometric_verification_desc)
val disablePinTitle = stringResource(R.string.disable_pin_verification_title) val disablePinTitle = stringResource(R.string.disable_pin_verification_title)
val disablePinDesc = stringResource(R.string.disable_pin_verification_desc)
val biometricFailedMessage = stringResource(R.string.biometric_failed) val biometricFailedMessage = stringResource(R.string.biometric_failed)
val biometricNoHardwareMessage = stringResource(R.string.biometric_error_no_hardware) val biometricNoHardwareMessage = stringResource(R.string.biometric_error_no_hardware)
val biometricHardwareUnavailableMessage = stringResource(R.string.biometric_error_hw_unavailable) val biometricHardwareUnavailableMessage = stringResource(R.string.biometric_error_hw_unavailable)
@@ -112,23 +110,23 @@ fun SecuritySettingsScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (isPinEnabled) { Row(
Row( modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically ) {
) { Column {
Column { Text(stringResource(R.string.biometric_unlock), style = MaterialTheme.typography.titleMedium)
Text(stringResource(R.string.biometric_unlock), style = MaterialTheme.typography.titleMedium) Text(
Text( stringResource(R.string.biometric_unlock_desc),
stringResource(R.string.biometric_unlock_desc), style = MaterialTheme.typography.bodyMedium,
style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.onSurfaceVariant )
) }
} Switch(
Switch( checked = isBiometricEnabled,
checked = isBiometricEnabled, enabled = isPinEnabled,
onCheckedChange = { checked -> onCheckedChange = { checked ->
if (checked) { if (checked) {
biometricAuthenticator.authenticate( biometricAuthenticator.authenticate(
mode = BiometricAuthMode.BiometricOnly, mode = BiometricAuthMode.BiometricOnly,
@@ -172,15 +170,21 @@ fun SecuritySettingsScreen(
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Card( if (isPinEnabled) {
onClick = { showLockTriggerDialog = true }, Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.selectable(
selected = false,
onClick = { showLockTriggerDialog = true }
)
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(modifier = Modifier.weight(1f)) {
modifier = Modifier.padding(16.dp)
) {
Text( Text(
stringResource(R.string.lock_trigger_title), stringResource(R.string.lock_trigger_title),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
@@ -190,13 +194,12 @@ fun SecuritySettingsScreen(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(lockTrigger.toLabelRes()),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary
)
} }
Text(
text = stringResource(lockTrigger.toLabelRes()),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary
)
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -233,10 +236,6 @@ fun SecuritySettingsScreen(
SecurityVerificationAction.DisableBiometric -> disableBiometricTitle SecurityVerificationAction.DisableBiometric -> disableBiometricTitle
SecurityVerificationAction.DisablePin -> disablePinTitle SecurityVerificationAction.DisablePin -> disablePinTitle
}, },
description = when (action) {
SecurityVerificationAction.DisableBiometric -> disableBiometricDesc
SecurityVerificationAction.DisablePin -> disablePinDesc
},
expectedPin = pinCode, expectedPin = pinCode,
onDismissRequest = { verificationAction = null }, onDismissRequest = { verificationAction = null },
onVerified = { onVerified = {
@@ -17,6 +17,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.airnan.a2fair.R import cn.airnan.a2fair.R
import cn.airnan.a2fair.core.security.PIN_LENGTH import cn.airnan.a2fair.core.security.PIN_LENGTH
@@ -24,7 +25,6 @@ import cn.airnan.a2fair.core.security.PIN_LENGTH
@Composable @Composable
fun SecurityVerificationDialog( fun SecurityVerificationDialog(
title: String, title: String,
description: String,
expectedPin: String?, expectedPin: String?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onVerified: () -> Unit onVerified: () -> Unit
@@ -32,7 +32,6 @@ fun SecurityVerificationDialog(
var enteredPin by remember { mutableStateOf("") } var enteredPin by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
val incorrectPinMessage = stringResource(R.string.incorrect_pin) val incorrectPinMessage = stringResource(R.string.incorrect_pin)
val pinHintMessage = stringResource(R.string.confirm_current_pin)
fun submitPin(candidate: String) { fun submitPin(candidate: String) {
if (!expectedPin.isNullOrEmpty() && candidate == expectedPin) { if (!expectedPin.isNullOrEmpty() && candidate == expectedPin) {
@@ -45,17 +44,19 @@ fun SecurityVerificationDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = { Text(text = title) }, title = {
Text(
text = title,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
},
text = { text = {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Spacer(modifier = Modifier.height(12.dp))
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
PinIndicatorRow( PinIndicatorRow(
filledCount = enteredPin.length, filledCount = enteredPin.length,
@@ -64,13 +65,15 @@ fun SecurityVerificationDialog(
) )
Text( Text(
text = errorMessage ?: pinHintMessage, text = errorMessage ?: "",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = if (errorMessage != null) { color = if (errorMessage != null) {
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
} else { } else {
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
} },
modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -103,13 +103,10 @@
<string name="try_again_in">请等待 %d 秒后重试</string> <string name="try_again_in">请等待 %d 秒后重试</string>
<string name="unlock_2fair">解锁 2FAir</string> <string name="unlock_2fair">解锁 2FAir</string>
<string name="biometric_prompt_subtitle">使用您的生物识别凭证登录</string> <string name="biometric_prompt_subtitle">使用您的生物识别凭证登录</string>
<string name="confirm_current_pin">请输入当前 PIN 码以继续</string>
<string name="enable_biometric_verification_title">验证生物识别</string> <string name="enable_biometric_verification_title">验证生物识别</string>
<string name="enable_biometric_verification_desc">启用生物识别解锁前,请先完成一次生物识别验证</string> <string name="enable_biometric_verification_desc">启用生物识别解锁前,请先完成一次生物识别验证</string>
<string name="disable_biometric_verification_title">确认关闭生物识别</string> <string name="disable_biometric_verification_title">确认关闭生物识别</string>
<string name="disable_biometric_verification_desc">关闭生物识别解锁前,请先输入当前 PIN 码。关闭后会保留 PIN 码。</string>
<string name="disable_pin_verification_title">确认关闭应用锁 PIN 码</string> <string name="disable_pin_verification_title">确认关闭应用锁 PIN 码</string>
<string name="disable_pin_verification_desc">关闭应用锁前,请先输入当前 PIN 码。关闭后会同时关闭生物识别并清除已保存的 PIN 码。</string>
<string name="biometric_error_no_hardware">此设备不支持生物识别</string> <string name="biometric_error_no_hardware">此设备不支持生物识别</string>
<string name="biometric_error_hw_unavailable">生物识别当前暂时不可用</string> <string name="biometric_error_hw_unavailable">生物识别当前暂时不可用</string>
<string name="biometric_error_none_enrolled">请先在系统设置中录入生物识别信息</string> <string name="biometric_error_none_enrolled">请先在系统设置中录入生物识别信息</string>
@@ -103,13 +103,10 @@
<string name="try_again_in">請等待 %d 秒後重試</string> <string name="try_again_in">請等待 %d 秒後重試</string>
<string name="unlock_2fair">解鎖 2FAir</string> <string name="unlock_2fair">解鎖 2FAir</string>
<string name="biometric_prompt_subtitle">使用您的生物辨識憑證登入</string> <string name="biometric_prompt_subtitle">使用您的生物辨識憑證登入</string>
<string name="confirm_current_pin">請輸入目前的 PIN 碼以繼續</string>
<string name="enable_biometric_verification_title">驗證生物辨識</string> <string name="enable_biometric_verification_title">驗證生物辨識</string>
<string name="enable_biometric_verification_desc">啟用生物辨識解鎖前,請先完成一次生物辨識驗證</string> <string name="enable_biometric_verification_desc">啟用生物辨識解鎖前,請先完成一次生物辨識驗證</string>
<string name="disable_biometric_verification_title">確認關閉生物辨識</string> <string name="disable_biometric_verification_title">確認關閉生物辨識</string>
<string name="disable_biometric_verification_desc">關閉生物辨識解鎖前,請先輸入目前的 PIN 碼。關閉後會保留 PIN 碼。</string>
<string name="disable_pin_verification_title">確認關閉應用程式 PIN 碼鎖定</string> <string name="disable_pin_verification_title">確認關閉應用程式 PIN 碼鎖定</string>
<string name="disable_pin_verification_desc">關閉應用程式鎖定前,請先輸入目前的 PIN 碼。關閉後會同時關閉生物辨識並清除已儲存的 PIN 碼。</string>
<string name="biometric_error_no_hardware">此裝置不支援生物辨識</string> <string name="biometric_error_no_hardware">此裝置不支援生物辨識</string>
<string name="biometric_error_hw_unavailable">生物辨識目前暫時無法使用</string> <string name="biometric_error_hw_unavailable">生物辨識目前暫時無法使用</string>
<string name="biometric_error_none_enrolled">請先在系統設定中錄入生物辨識資訊</string> <string name="biometric_error_none_enrolled">請先在系統設定中錄入生物辨識資訊</string>
-3
View File
@@ -103,13 +103,10 @@
<string name="try_again_in">Try again in %ds</string> <string name="try_again_in">Try again in %ds</string>
<string name="unlock_2fair">Unlock 2FAir</string> <string name="unlock_2fair">Unlock 2FAir</string>
<string name="biometric_prompt_subtitle">Log in using your biometric credential</string> <string name="biometric_prompt_subtitle">Log in using your biometric credential</string>
<string name="confirm_current_pin">Enter your current PIN to continue</string>
<string name="enable_biometric_verification_title">Verify biometric unlock</string> <string name="enable_biometric_verification_title">Verify biometric unlock</string>
<string name="enable_biometric_verification_desc">Authenticate once with biometrics before enabling biometric unlock</string> <string name="enable_biometric_verification_desc">Authenticate once with biometrics before enabling biometric unlock</string>
<string name="disable_biometric_verification_title">Confirm to turn off biometric unlock</string> <string name="disable_biometric_verification_title">Confirm to turn off biometric unlock</string>
<string name="disable_biometric_verification_desc">Enter your current PIN before turning off biometric unlock. Your PIN will remain enabled.</string>
<string name="disable_pin_verification_title">Confirm to turn off app lock PIN</string> <string name="disable_pin_verification_title">Confirm to turn off app lock PIN</string>
<string name="disable_pin_verification_desc">Enter your current PIN before turning off app lock. This will also turn off biometric unlock and clear the saved PIN.</string>
<string name="biometric_error_no_hardware">This device does not support biometric authentication</string> <string name="biometric_error_no_hardware">This device does not support biometric authentication</string>
<string name="biometric_error_hw_unavailable">Biometric authentication is temporarily unavailable</string> <string name="biometric_error_hw_unavailable">Biometric authentication is temporarily unavailable</string>
<string name="biometric_error_none_enrolled">Set up biometrics in system settings before enabling biometric unlock</string> <string name="biometric_error_none_enrolled">Set up biometrics in system settings before enabling biometric unlock</string>