修正应用锁逻辑错误
This commit is contained in:
@@ -61,6 +61,10 @@ class MainActivity : FragmentActivity() {
|
||||
val isFlagSecureEnabled by settingsRepo.isFlagSecureEnabled.collectAsStateWithLifecycle(initialValue = false)
|
||||
val isLocked by appLockManager.isLocked.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
appLockManager.checkInitialLockState()
|
||||
}
|
||||
|
||||
LaunchedEffect(isFlagSecureEnabled) {
|
||||
if (isFlagSecureEnabled) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
@@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -30,40 +29,84 @@ class AppLockManager(
|
||||
val isLocked: StateFlow<Boolean> = _isLocked.asStateFlow()
|
||||
|
||||
private var pendingLockJob: Job? = null
|
||||
private var lockDeadlineAtMillis: Long? = null
|
||||
|
||||
private val screenOffReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
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 {
|
||||
appContext.registerReceiver(screenOffReceiver, IntentFilter(Intent.ACTION_SCREEN_OFF))
|
||||
appContext.registerReceiver(screenOnReceiver, IntentFilter(Intent.ACTION_SCREEN_ON))
|
||||
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() {
|
||||
pendingLockJob?.cancel()
|
||||
pendingLockJob = null
|
||||
lockDeadlineAtMillis = null
|
||||
scope.launch {
|
||||
val appLockSettings = settingsRepository.appLockSettings.first()
|
||||
_isLocked.value = appLockSettings.isAppLockEnabled
|
||||
val settings = settingsRepository.appLockSettings.first()
|
||||
_isLocked.value = settings.isAppLockEnabled
|
||||
}
|
||||
}
|
||||
|
||||
fun unlock() {
|
||||
pendingLockJob?.cancel()
|
||||
pendingLockJob = null
|
||||
lockDeadlineAtMillis = null
|
||||
scope.launch {
|
||||
settingsRepository.clearLastBackgroundTime()
|
||||
}
|
||||
_isLocked.value = false
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
super.onStart(owner)
|
||||
cancelPendingLock()
|
||||
scope.launch {
|
||||
refreshLockStateWhenForegrounded()
|
||||
}
|
||||
@@ -71,7 +114,7 @@ class AppLockManager(
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
scheduleLock()
|
||||
recordBackgroundAndScheduleLock()
|
||||
}
|
||||
|
||||
private fun observeSettings() {
|
||||
@@ -80,65 +123,89 @@ class AppLockManager(
|
||||
if (!settings.isAppLockEnabled) {
|
||||
pendingLockJob?.cancel()
|
||||
pendingLockJob = null
|
||||
lockDeadlineAtMillis = null
|
||||
_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 = null
|
||||
|
||||
scope.launch {
|
||||
val appLockSettings = settingsRepository.appLockSettings.first()
|
||||
if (!appLockSettings.isAppLockEnabled) {
|
||||
lockDeadlineAtMillis = null
|
||||
val settings = settingsRepository.appLockSettings.first()
|
||||
if (!settings.isAppLockEnabled) {
|
||||
_isLocked.value = false
|
||||
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) {
|
||||
lockDeadlineAtMillis = null
|
||||
_isLocked.value = true
|
||||
return@launch
|
||||
}
|
||||
|
||||
val deadline = System.currentTimeMillis() + delayMillis
|
||||
lockDeadlineAtMillis = deadline
|
||||
pendingLockJob = scope.launch {
|
||||
delay(delayMillis)
|
||||
val latestSettings = settingsRepository.appLockSettings.first()
|
||||
if (latestSettings.isAppLockEnabled && lockDeadlineAtMillis == deadline) {
|
||||
if (latestSettings.isAppLockEnabled) {
|
||||
_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() {
|
||||
val appLockSettings = settingsRepository.appLockSettings.first()
|
||||
if (!appLockSettings.isAppLockEnabled) {
|
||||
val settings = settingsRepository.appLockSettings.first()
|
||||
if (!settings.isAppLockEnabled) {
|
||||
unlock()
|
||||
return
|
||||
}
|
||||
|
||||
val deadline = lockDeadlineAtMillis
|
||||
if (deadline == null) {
|
||||
return
|
||||
val lastBg = settingsRepository.getLastBackgroundTime()
|
||||
if (lastBg <= 0L) return
|
||||
|
||||
val delayMillis = settings.lockTrigger.delayMillis
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val shouldLock = if (delayMillis <= 0L) {
|
||||
true
|
||||
} else {
|
||||
now >= lastBg + delayMillis
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() >= deadline) {
|
||||
pendingLockJob?.cancel()
|
||||
pendingLockJob = null
|
||||
lockDeadlineAtMillis = null
|
||||
if (shouldLock) {
|
||||
cancelPendingLock()
|
||||
_isLocked.value = true
|
||||
} else {
|
||||
pendingLockJob?.cancel()
|
||||
pendingLockJob = null
|
||||
lockDeadlineAtMillis = null
|
||||
// Still within the grace period — cancel the pending in-memory lock
|
||||
cancelPendingLock()
|
||||
settingsRepository.clearLastBackgroundTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
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 ->
|
||||
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 LOCKOUT_END_TIME = longPreferencesKey("lockout_end_time")
|
||||
val APP_LOCK_TRIGGER = intPreferencesKey("app_lock_trigger")
|
||||
val LAST_BACKGROUND_TIME = longPreferencesKey("last_background_time")
|
||||
val THEME_MODE = intPreferencesKey("theme_mode")
|
||||
val DYNAMIC_COLOR = booleanPreferencesKey("dynamic_color")
|
||||
val SEED_COLOR = intPreferencesKey("seed_color")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -15,9 +17,12 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -45,12 +50,23 @@ fun PinIndicatorRow(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
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(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(14.dp)
|
||||
.scale(scale.value)
|
||||
.clip(CircleShape)
|
||||
.background(if (index < filledCount) activeColor else inactiveColor)
|
||||
.background(if (isFilled) activeColor else inactiveColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -88,10 +104,10 @@ fun PinNumberPad(
|
||||
onClick = { onDigit(digit) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
.height(72.dp),
|
||||
enabled = isEnabled
|
||||
) {
|
||||
Text(text = digit, style = MaterialTheme.typography.titleLarge)
|
||||
Text(text = digit, style = MaterialTheme.typography.headlineMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +122,7 @@ fun PinNumberPad(
|
||||
onClick = onReset,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
.height(72.dp),
|
||||
enabled = isEnabled && isResetEnabled
|
||||
) {
|
||||
Text(text = resetLabel)
|
||||
@@ -116,7 +132,7 @@ fun PinNumberPad(
|
||||
onClick = { onDigit("0") },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
.height(72.dp),
|
||||
enabled = isEnabled
|
||||
) {
|
||||
Text(text = "0", style = MaterialTheme.typography.titleLarge)
|
||||
@@ -126,7 +142,7 @@ fun PinNumberPad(
|
||||
onClick = onDelete,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp)
|
||||
.height(72.dp)
|
||||
.semantics { contentDescription = deleteLabel },
|
||||
enabled = isEnabled && isDeleteEnabled
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.airnan.a2fair.ui.settings.security
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
@@ -38,7 +39,7 @@ fun PinSetupScreen(
|
||||
R.string.pin_digits_remaining,
|
||||
PIN_LENGTH - currentPin.length
|
||||
)
|
||||
else -> pinRequirementMsg
|
||||
else -> ""
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -68,13 +69,6 @@ fun PinSetupScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = pinRequirementMsg,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
PinIndicatorRow(
|
||||
filledCount = currentPin.length,
|
||||
isError = errorMessage != null
|
||||
@@ -90,7 +84,7 @@ fun PinSetupScreen(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.height(36.dp))
|
||||
|
||||
PinNumberPad(
|
||||
resetLabel = if (step == 1) {
|
||||
@@ -131,33 +125,44 @@ fun PinSetupScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (step == 1) {
|
||||
if (pin.length != PIN_LENGTH) {
|
||||
errorMessage = pinTooShortMsg
|
||||
} else {
|
||||
confirmPin = ""
|
||||
errorMessage = null
|
||||
step = 2
|
||||
}
|
||||
} else {
|
||||
if (confirmPin.length != PIN_LENGTH) {
|
||||
errorMessage = pinTooShortMsg
|
||||
} else if (pin == confirmPin) {
|
||||
scope.launch {
|
||||
settingsRepository.enablePin(pin)
|
||||
onNavigateBack()
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (step == 1) {
|
||||
if (pin.length != PIN_LENGTH) {
|
||||
errorMessage = pinTooShortMsg
|
||||
} else {
|
||||
confirmPin = ""
|
||||
errorMessage = null
|
||||
step = 2
|
||||
}
|
||||
} else {
|
||||
errorMessage = pinsDoNotMatchMsg
|
||||
confirmPin = ""
|
||||
if (confirmPin.length != PIN_LENGTH) {
|
||||
errorMessage = pinTooShortMsg
|
||||
} else if (pin == confirmPin) {
|
||||
scope.launch {
|
||||
settingsRepository.enablePin(pin)
|
||||
onNavigateBack()
|
||||
}
|
||||
} else {
|
||||
errorMessage = pinsDoNotMatchMsg
|
||||
confirmPin = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(if (step == 1) stringResource(R.string.next) else stringResource(R.string.confirm))
|
||||
},
|
||||
modifier = Modifier
|
||||
.height(56.dp)
|
||||
.widthIn(min = 160.dp),
|
||||
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 enableBiometricSubtitle = stringResource(R.string.enable_biometric_verification_desc)
|
||||
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 disablePinDesc = stringResource(R.string.disable_pin_verification_desc)
|
||||
val biometricFailedMessage = stringResource(R.string.biometric_failed)
|
||||
val biometricNoHardwareMessage = stringResource(R.string.biometric_error_no_hardware)
|
||||
val biometricHardwareUnavailableMessage = stringResource(R.string.biometric_error_hw_unavailable)
|
||||
@@ -112,23 +110,23 @@ fun SecuritySettingsScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (isPinEnabled) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(stringResource(R.string.biometric_unlock), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
stringResource(R.string.biometric_unlock_desc),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = isBiometricEnabled,
|
||||
onCheckedChange = { checked ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(stringResource(R.string.biometric_unlock), style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
stringResource(R.string.biometric_unlock_desc),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = isBiometricEnabled,
|
||||
enabled = isPinEnabled,
|
||||
onCheckedChange = { checked ->
|
||||
if (checked) {
|
||||
biometricAuthenticator.authenticate(
|
||||
mode = BiometricAuthMode.BiometricOnly,
|
||||
@@ -172,15 +170,21 @@ fun SecuritySettingsScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Card(
|
||||
onClick = { showLockTriggerDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
if (isPinEnabled) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = false,
|
||||
onClick = { showLockTriggerDialog = true }
|
||||
)
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
stringResource(R.string.lock_trigger_title),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
@@ -190,13 +194,12 @@ fun SecuritySettingsScreen(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
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))
|
||||
@@ -233,10 +236,6 @@ fun SecuritySettingsScreen(
|
||||
SecurityVerificationAction.DisableBiometric -> disableBiometricTitle
|
||||
SecurityVerificationAction.DisablePin -> disablePinTitle
|
||||
},
|
||||
description = when (action) {
|
||||
SecurityVerificationAction.DisableBiometric -> disableBiometricDesc
|
||||
SecurityVerificationAction.DisablePin -> disablePinDesc
|
||||
},
|
||||
expectedPin = pinCode,
|
||||
onDismissRequest = { verificationAction = null },
|
||||
onVerified = {
|
||||
|
||||
+13
-10
@@ -17,6 +17,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.airnan.a2fair.R
|
||||
import cn.airnan.a2fair.core.security.PIN_LENGTH
|
||||
@@ -24,7 +25,6 @@ import cn.airnan.a2fair.core.security.PIN_LENGTH
|
||||
@Composable
|
||||
fun SecurityVerificationDialog(
|
||||
title: String,
|
||||
description: String,
|
||||
expectedPin: String?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onVerified: () -> Unit
|
||||
@@ -32,7 +32,6 @@ fun SecurityVerificationDialog(
|
||||
var enteredPin by remember { mutableStateOf("") }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
val incorrectPinMessage = stringResource(R.string.incorrect_pin)
|
||||
val pinHintMessage = stringResource(R.string.confirm_current_pin)
|
||||
|
||||
fun submitPin(candidate: String) {
|
||||
if (!expectedPin.isNullOrEmpty() && candidate == expectedPin) {
|
||||
@@ -45,17 +44,19 @@ fun SecurityVerificationDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = title) },
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
PinIndicatorRow(
|
||||
filledCount = enteredPin.length,
|
||||
@@ -64,13 +65,15 @@ fun SecurityVerificationDialog(
|
||||
)
|
||||
|
||||
Text(
|
||||
text = errorMessage ?: pinHintMessage,
|
||||
text = errorMessage ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = if (errorMessage != null) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
@@ -103,13 +103,10 @@
|
||||
<string name="try_again_in">请等待 %d 秒后重试</string>
|
||||
<string name="unlock_2fair">解锁 2FAir</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_desc">启用生物识别解锁前,请先完成一次生物识别验证</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_desc">关闭应用锁前,请先输入当前 PIN 码。关闭后会同时关闭生物识别并清除已保存的 PIN 码。</string>
|
||||
<string name="biometric_error_no_hardware">此设备不支持生物识别</string>
|
||||
<string name="biometric_error_hw_unavailable">生物识别当前暂时不可用</string>
|
||||
<string name="biometric_error_none_enrolled">请先在系统设置中录入生物识别信息</string>
|
||||
|
||||
@@ -103,13 +103,10 @@
|
||||
<string name="try_again_in">請等待 %d 秒後重試</string>
|
||||
<string name="unlock_2fair">解鎖 2FAir</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_desc">啟用生物辨識解鎖前,請先完成一次生物辨識驗證</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_desc">關閉應用程式鎖定前,請先輸入目前的 PIN 碼。關閉後會同時關閉生物辨識並清除已儲存的 PIN 碼。</string>
|
||||
<string name="biometric_error_no_hardware">此裝置不支援生物辨識</string>
|
||||
<string name="biometric_error_hw_unavailable">生物辨識目前暫時無法使用</string>
|
||||
<string name="biometric_error_none_enrolled">請先在系統設定中錄入生物辨識資訊</string>
|
||||
|
||||
@@ -103,13 +103,10 @@
|
||||
<string name="try_again_in">Try again in %ds</string>
|
||||
<string name="unlock_2fair">Unlock 2FAir</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_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_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_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_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>
|
||||
|
||||
Reference in New Issue
Block a user