修正应用锁逻辑错误
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
+13
-10
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user