From be613749c42fa00918c32b7d0a96dd6e4edce9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=81=E5=86=9C=E9=98=BF=E6=A5=A0?= Date: Wed, 17 Jun 2026 09:53:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=BA=94=E7=94=A8=E9=94=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/airnan/a2fair/MainActivity.kt | 4 + .../a2fair/core/security/AppLockManager.kt | 127 +++++++++++++----- .../data/repository/SettingsRepository.kt | 18 +++ .../settings/security/PinInputComponents.kt | 28 +++- .../ui/settings/security/PinSetupScreen.kt | 71 +++++----- .../security/SecuritySettingsScreen.kt | 71 +++++----- .../security/SecurityVerificationDialog.kt | 23 ++-- app/src/main/res/values-zh-rCN/strings.xml | 3 - app/src/main/res/values-zh-rTW/strings.xml | 3 - app/src/main/res/values/strings.xml | 3 - 10 files changed, 227 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/cn/airnan/a2fair/MainActivity.kt b/app/src/main/java/cn/airnan/a2fair/MainActivity.kt index f05480f..f05db43 100644 --- a/app/src/main/java/cn/airnan/a2fair/MainActivity.kt +++ b/app/src/main/java/cn/airnan/a2fair/MainActivity.kt @@ -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) diff --git a/app/src/main/java/cn/airnan/a2fair/core/security/AppLockManager.kt b/app/src/main/java/cn/airnan/a2fair/core/security/AppLockManager.kt index 8b15c3c..a71fb86 100644 --- a/app/src/main/java/cn/airnan/a2fair/core/security/AppLockManager.kt +++ b/app/src/main/java/cn/airnan/a2fair/core/security/AppLockManager.kt @@ -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 = _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() } } } diff --git a/app/src/main/java/cn/airnan/a2fair/data/repository/SettingsRepository.kt b/app/src/main/java/cn/airnan/a2fair/data/repository/SettingsRepository.kt index 8042071..87b8dee 100644 --- a/app/src/main/java/cn/airnan/a2fair/data/repository/SettingsRepository.kt +++ b/app/src/main/java/cn/airnan/a2fair/data/repository/SettingsRepository.kt @@ -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 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 = 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") diff --git a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinInputComponents.kt b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinInputComponents.kt index ee77bd8..e73716d 100644 --- a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinInputComponents.kt +++ b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinInputComponents.kt @@ -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 ) { diff --git a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinSetupScreen.kt b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinSetupScreen.kt index e7c4db7..8f1c098 100644 --- a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinSetupScreen.kt +++ b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/PinSetupScreen.kt @@ -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 + ) + } } } } diff --git a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecuritySettingsScreen.kt b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecuritySettingsScreen.kt index b384ba0..3894519 100644 --- a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecuritySettingsScreen.kt +++ b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecuritySettingsScreen.kt @@ -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 = { diff --git a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecurityVerificationDialog.kt b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecurityVerificationDialog.kt index 96fe5da..ddb7081 100644 --- a/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecurityVerificationDialog.kt +++ b/app/src/main/java/cn/airnan/a2fair/ui/settings/security/SecurityVerificationDialog.kt @@ -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(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)) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4467b16..8d1c45e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -103,13 +103,10 @@ 请等待 %d 秒后重试 解锁 2FAir 使用您的生物识别凭证登录 - 请输入当前 PIN 码以继续 验证生物识别 启用生物识别解锁前,请先完成一次生物识别验证 确认关闭生物识别 - 关闭生物识别解锁前,请先输入当前 PIN 码。关闭后会保留 PIN 码。 确认关闭应用锁 PIN 码 - 关闭应用锁前,请先输入当前 PIN 码。关闭后会同时关闭生物识别并清除已保存的 PIN 码。 此设备不支持生物识别 生物识别当前暂时不可用 请先在系统设置中录入生物识别信息 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d9b5fe1..a921329 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -103,13 +103,10 @@ 請等待 %d 秒後重試 解鎖 2FAir 使用您的生物辨識憑證登入 - 請輸入目前的 PIN 碼以繼續 驗證生物辨識 啟用生物辨識解鎖前,請先完成一次生物辨識驗證 確認關閉生物辨識 - 關閉生物辨識解鎖前,請先輸入目前的 PIN 碼。關閉後會保留 PIN 碼。 確認關閉應用程式 PIN 碼鎖定 - 關閉應用程式鎖定前,請先輸入目前的 PIN 碼。關閉後會同時關閉生物辨識並清除已儲存的 PIN 碼。 此裝置不支援生物辨識 生物辨識目前暫時無法使用 請先在系統設定中錄入生物辨識資訊 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4946bb..ac9959a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,13 +103,10 @@ Try again in %ds Unlock 2FAir Log in using your biometric credential - Enter your current PIN to continue Verify biometric unlock Authenticate once with biometrics before enabling biometric unlock Confirm to turn off biometric unlock - Enter your current PIN before turning off biometric unlock. Your PIN will remain enabled. Confirm to turn off app lock PIN - Enter your current PIN before turning off app lock. This will also turn off biometric unlock and clear the saved PIN. This device does not support biometric authentication Biometric authentication is temporarily unavailable Set up biometrics in system settings before enabling biometric unlock