PIN : start branching logic

This commit is contained in:
ganfra
2023-10-24 14:19:52 +02:00
parent b0f27c111e
commit 645c699a6b
20 changed files with 316 additions and 78 deletions

View File

@@ -32,4 +32,9 @@ object LockScreenConfig {
* The size of the PIN.
*/
const val PIN_SIZE = 4
/**
* Number of attempts before the user is logged out.
*/
const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3
}

View File

@@ -23,5 +23,4 @@ interface LockScreenStateService {
suspend fun entersForeground()
suspend fun entersBackground()
suspend fun unlock()
}

View File

@@ -24,9 +24,11 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode
import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsNode
import io.element.android.features.lockscreen.impl.setup.SetupPinNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
@@ -74,7 +76,7 @@ class LockScreenFlowNode @AssistedInject constructor(
createNode<SetupPinNode>(buildContext)
}
NavTarget.Settings -> {
createNode<LockScreenSettingsNode>(buildContext)
createNode<LockScreenSettingsFlowNode>(buildContext)
}
}
}

View File

@@ -22,17 +22,30 @@ import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.EncryptionResult
import io.element.android.libraries.cryptography.api.SecretKeyProvider
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
private const val SECRET_KEY_ALIAS = "SECRET_KEY_ALIAS_PIN_CODE"
private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE"
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class DefaultPinCodeManager @Inject constructor(
private val secretKeyProvider: SecretKeyProvider,
private val encryptionDecryptionService: EncryptionDecryptionService,
private val pinCodeStore: PinCodeStore,
) : PinCodeManager {
private val callbacks = CopyOnWriteArrayList<PinCodeManager.Callback>()
override fun addCallback(callback: PinCodeManager.Callback) {
callbacks.add(callback)
}
override fun removeCallback(callback: PinCodeManager.Callback) {
callbacks.remove(callback)
}
override suspend fun isPinCodeAvailable(): Boolean {
return pinCodeStore.hasPinCode()
}
@@ -41,6 +54,7 @@ class DefaultPinCodeManager @Inject constructor(
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
callbacks.forEach { it.onPinCodeCreated() }
}
override suspend fun verifyPinCode(pinCode: String): Boolean {
@@ -48,7 +62,17 @@ class DefaultPinCodeManager @Inject constructor(
return try {
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
decryptedPinCode.contentEquals(pinCode.toByteArray())
val pinCodeToCheck = pinCode.toByteArray()
decryptedPinCode.contentEquals(pinCodeToCheck).also { isPinCodeCorrect ->
if (isPinCodeCorrect) {
pinCodeStore.resetCounter()
callbacks.forEach { callback ->
callback.onPinCodeVerified()
}
} else {
pinCodeStore.onWrongPin()
}
}
} catch (failure: Throwable) {
false
}
@@ -56,17 +80,11 @@ class DefaultPinCodeManager @Inject constructor(
override suspend fun deletePinCode() {
pinCodeStore.deleteEncryptedPinCode()
pinCodeStore.resetCounter()
callbacks.forEach { it.onPinCodeRemoved() }
}
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
return pinCodeStore.getRemainingPinCodeAttemptsNumber()
}
override suspend fun onWrongPin(): Int {
return pinCodeStore.onWrongPin()
}
override suspend fun resetCounter() {
pinCodeStore.resetCounter()
}
}

View File

@@ -21,6 +21,37 @@ package io.element.android.features.lockscreen.impl.pin
* Implementation should take care of encrypting the pin code and storing it.
*/
interface PinCodeManager {
/**
* Callbacks for pin code management events.
*/
interface Callback {
/**
* Called when the pin code is verified.
*/
fun onPinCodeVerified() = Unit
/**
* Called when the pin code is created.
*/
fun onPinCodeCreated() = Unit
/**
* Called when the pin code is removed.
*/
fun onPinCodeRemoved() = Unit
}
/**
* Register a callback to be notified of pin code management events.
*/
fun addCallback(callback: Callback)
/**
* Unregister callback to be notified of pin code management events.
*/
fun removeCallback(callback: Callback)
/**
* @return true if a pin code is available.
*/
@@ -46,16 +77,4 @@ interface PinCodeManager {
* @return the number of remaining attempts before the pin code is blocked.
*/
suspend fun getRemainingPinCodeAttemptsNumber(): Int
/**
* Should be called when the pin code is incorrect.
* Will decrement the remaining attempts number.
* @return the number of remaining attempts before the pin code is blocked.
*/
suspend fun onWrongPin(): Int
/**
* Resets the counter of attempts for PIN code.
*/
suspend fun resetCounter()
}

View File

@@ -19,6 +19,7 @@ package io.element.android.features.lockscreen.impl.pin.storage
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.LockScreenConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.DefaultPreferences
@@ -31,7 +32,6 @@ import javax.inject.Inject
private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY"
private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
@@ -57,8 +57,6 @@ class SharedPreferencesPinCodeStore @Inject constructor(
}
override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) {
// Also reset the counters
resetCounter()
sharedPreferences.edit {
remove(ENCODED_PIN_CODE_KEY)
}
@@ -72,14 +70,12 @@ class SharedPreferencesPinCodeStore @Inject constructor(
}
override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) {
mutex.withLock {
sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
}
sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
}
override suspend fun onWrongPin(): Int = withContext(dispatchers.io) {
mutex.withLock {
val remaining = getRemainingPinCodeAttemptsNumber() - 1
val remaining = (getRemainingPinCodeAttemptsNumber() - 1).coerceAtLeast(0)
sharedPreferences.edit {
putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining)
}

View File

@@ -20,6 +20,5 @@ sealed interface LockScreenSettingsEvents {
data object RemovePin : LockScreenSettingsEvents
data object ConfirmRemovePin : LockScreenSettingsEvents
data object CancelRemovePin : LockScreenSettingsEvents
data object ChangePin : LockScreenSettingsEvents
data object ToggleBiometric : LockScreenSettingsEvents
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.settings
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.setup.SetupPinNode
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
class LockScreenSettingsFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val pinCodeManager: PinCodeManager,
) : BackstackNode<LockScreenSettingsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Unknown,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins,
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Unknown : NavTarget
@Parcelize
data object Unlock : NavTarget
@Parcelize
data object Setup : NavTarget
@Parcelize
data object Settings : NavTarget
}
private val pinCodeManagerCallback = object : PinCodeManager.Callback {
override fun onPinCodeVerified() {
backstack.newRoot(NavTarget.Settings)
}
override fun onPinCodeCreated() {
backstack.newRoot(NavTarget.Settings)
}
override fun onPinCodeRemoved() {
navigateUp()
}
}
init {
lifecycleScope.launch {
if (pinCodeManager.isPinCodeAvailable()) {
backstack.newRoot(NavTarget.Unlock)
} else {
backstack.newRoot(NavTarget.Setup)
}
}
lifecycle.subscribe(
onCreate = {
pinCodeManager.addCallback(pinCodeManagerCallback)
},
onDestroy = {
pinCodeManager.removeCallback(pinCodeManagerCallback)
}
)
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
NavTarget.Unlock -> {
createNode<PinUnlockNode>(buildContext)
}
NavTarget.Setup -> {
createNode<SetupPinNode>(buildContext)
}
NavTarget.Settings -> {
val callback = object : LockScreenSettingsNode.Callback {
override fun onChangePinClicked() {
backstack.push(NavTarget.Setup)
}
}
createNode<LockScreenSettingsNode>(buildContext, plugins = listOf(callback))
}
NavTarget.Unknown -> node(buildContext) { }
}
}
@Composable
override fun View(modifier: Modifier) {
Children(
navModel = backstack,
modifier = modifier,
transitionHandler = rememberDefaultTransitionHandler(),
)
}
}

View File

@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
@@ -33,12 +34,22 @@ class LockScreenSettingsNode @AssistedInject constructor(
private val presenter: LockScreenSettingsPresenter,
) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onChangePinClicked()
}
private fun onChangePinClicked() {
plugins<Callback>().forEach { it.onChangePinClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LockScreenSettingsView(
state = state,
modifier = modifier
onBackPressed = this::navigateUp,
onChangePinClicked = this::onChangePinClicked,
modifier = modifier,
)
}
}

View File

@@ -17,41 +17,65 @@
package io.element.android.features.lockscreen.impl.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class LockScreenSettingsPresenter @Inject constructor() : Presenter<LockScreenSettingsState> {
class LockScreenSettingsPresenter @Inject constructor(
private val pinCodeManager: PinCodeManager,
private val coroutineScope: CoroutineScope,
) : Presenter<LockScreenSettingsState> {
@Composable
override fun present(): LockScreenSettingsState {
var triggerComputation by remember {
mutableIntStateOf(0)
}
var showRemovePinOption by remember {
mutableStateOf(false)
}
var isBiometricEnabled by remember {
mutableStateOf(false)
}
var showRemovePinConfirmation by remember {
mutableStateOf(false)
}
LaunchedEffect(triggerComputation) {
showRemovePinOption = !LockScreenConfig.IS_PIN_MANDATORY && pinCodeManager.isPinCodeAvailable()
}
fun handleEvents(event: LockScreenSettingsEvents) {
when (event) {
LockScreenSettingsEvents.CancelRemovePin -> TODO()
LockScreenSettingsEvents.ChangePin -> TODO()
LockScreenSettingsEvents.ConfirmRemovePin -> TODO()
LockScreenSettingsEvents.RemovePin -> TODO()
LockScreenSettingsEvents.ToggleBiometric -> TODO()
LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false
LockScreenSettingsEvents.ConfirmRemovePin -> {
coroutineScope.launch {
showRemovePinConfirmation = false
pinCodeManager.deletePinCode()
triggerComputation++
}
}
LockScreenSettingsEvents.RemovePin -> showRemovePinConfirmation = true
LockScreenSettingsEvents.ToggleBiometric -> {
//TODO branch biometric logic
}
}
}
return LockScreenSettingsState(
isPinMandatory = LockScreenConfig.IS_PIN_MANDATORY,
showRemovePinOption = showRemovePinOption,
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
eventSink = ::handleEvents
)
}
}

View File

@@ -17,7 +17,7 @@
package io.element.android.features.lockscreen.impl.settings
data class LockScreenSettingsState(
val isPinMandatory: Boolean,
val showRemovePinOption: Boolean,
val isBiometricEnabled: Boolean,
val showRemovePinConfirmation: Boolean,
val eventSink: (LockScreenSettingsEvents) -> Unit

View File

@@ -32,7 +32,7 @@ fun aLockScreenSettingsState(
isBiometricEnabled: Boolean = false,
showRemovePinConfirmation: Boolean = false,
) = LockScreenSettingsState(
isPinMandatory = isLockMandatory,
showRemovePinOption = isLockMandatory,
isBiometricEnabled = isBiometricEnabled,
showRemovePinConfirmation = showRemovePinConfirmation,
eventSink = {}

View File

@@ -34,21 +34,22 @@ import io.element.android.libraries.theme.ElementTheme
@Composable
fun LockScreenSettingsView(
state: LockScreenSettingsState,
onChangePinClicked: () -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
PreferencePage(
title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock),
onBackPressed = onBackPressed,
modifier = modifier
) {
PreferenceCategory(showDivider = false) {
PreferenceText(
title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
onClick = {
state.eventSink(LockScreenSettingsEvents.ChangePin)
}
onClick = onChangePinClicked
)
PreferenceDivider()
if (!state.isPinMandatory) {
if (state.showRemovePinOption) {
PreferenceText(
title = stringResource(id = R.string.screen_app_lock_settings_remove_pin),
tintColor = ElementTheme.colors.textCriticalPrimary,
@@ -80,6 +81,10 @@ internal fun LockScreenSettingsViewPreview(
@PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState,
) {
ElementPreview {
LockScreenSettingsView(state)
LockScreenSettingsView(
state = state,
onChangePinClicked = {},
onBackPressed = {},
)
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
@@ -34,6 +35,7 @@ import javax.inject.Inject
class SetupPinPresenter @Inject constructor(
private val pinValidator: PinValidator,
private val buildMeta: BuildMeta,
private val pinCodeManager: PinCodeManager,
) : Presenter<SetupPinState> {
@Composable
@@ -68,7 +70,7 @@ class SetupPinPresenter @Inject constructor(
LaunchedEffect(confirmPinEntry) {
if (confirmPinEntry.isComplete()) {
if (confirmPinEntry == choosePinEntry) {
//TODO save in db and navigate to next screen
pinCodeManager.createPinCode(confirmPinEntry.toText())
} else {
setupPinFailure = SetupPinFailure.PinsDontMatch
}

View File

@@ -19,12 +19,15 @@ package io.element.android.features.lockscreen.impl.state
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.lockscreen.api.LockScreenState
import io.element.android.features.lockscreen.api.LockScreenStateService
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -36,6 +39,8 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLockScreenStateService @Inject constructor(
private val featureFlagService: FeatureFlagService,
private val pinCodeManager: PinCodeManager,
private val coroutineScope: CoroutineScope,
) : LockScreenStateService {
private val _lockScreenState = MutableStateFlow<LockScreenState>(LockScreenState.Unlocked)
@@ -43,10 +48,13 @@ class DefaultLockScreenStateService @Inject constructor(
private var lockJob: Job? = null
override suspend fun unlock() {
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) {
_lockScreenState.value = LockScreenState.Unlocked
}
init {
pinCodeManager.addCallback(object : PinCodeManager.Callback {
override fun onPinCodeVerified() {
_lockScreenState.value = LockScreenState.Unlocked
}
})
coroutineScope.lockIfNeeded()
}
override suspend fun entersForeground() {
@@ -54,11 +62,13 @@ class DefaultLockScreenStateService @Inject constructor(
}
override suspend fun entersBackground() = coroutineScope {
lockJob = launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) {
//delay(GRACE_PERIOD_IN_MILLIS)
_lockScreenState.value = LockScreenState.Locked
}
lockJob = lockIfNeeded()
}
private fun CoroutineScope.lockIfNeeded(delayInMillis: Long = 0L) = launch {
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && pinCodeManager.isPinCodeAvailable()) {
delay(delayInMillis)
_lockScreenState.value = LockScreenState.Locked
}
}
}

View File

@@ -17,24 +17,22 @@
package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import io.element.android.appconfig.LockScreenConfig
import io.element.android.features.lockscreen.api.LockScreenStateService
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class PinUnlockPresenter @Inject constructor(
private val pinStateService: LockScreenStateService,
private val coroutineScope: CoroutineScope,
private val pinCodeManager: PinCodeManager,
) : Presenter<PinUnlockState> {
@Composable
@@ -43,9 +41,8 @@ class PinUnlockPresenter @Inject constructor(
//TODO fetch size from db
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
}
var remainingAttempts by rememberSaveable {
//TODO fetch from db
mutableIntStateOf(3)
var remainingAttempts by remember {
mutableStateOf<Async<Int>>(Async.Uninitialized)
}
var showWrongPinTitle by rememberSaveable {
mutableStateOf(false)
@@ -54,14 +51,25 @@ class PinUnlockPresenter @Inject constructor(
mutableStateOf(false)
}
LaunchedEffect(pinEntry) {
if (pinEntry.isComplete()) {
val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText())
if (!isVerified) {
pinEntry = pinEntry.clear()
showWrongPinTitle = true
}
}
val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber()
remainingAttempts = Async.Success(remainingAttemptsNumber)
if (remainingAttemptsNumber == 0) {
showSignOutPrompt = true
}
}
fun handleEvents(event: PinUnlockEvents) {
when (event) {
is PinUnlockEvents.OnPinKeypadPressed -> {
pinEntry = pinEntry.process(event.pinKeypadModel)
if (pinEntry.isComplete()) {
//TODO check pin with PinCodeManager
coroutineScope.launch { pinStateService.unlock() }
}
}
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false

View File

@@ -17,13 +17,14 @@
package io.element.android.features.lockscreen.impl.unlock
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async
data class PinUnlockState(
val pinEntry: PinEntry,
val showWrongPinTitle: Boolean,
val remainingAttempts: Int,
val remainingAttempts: Async<Int>,
val showSignOutPrompt: Boolean,
val eventSink: (PinUnlockEvents) -> Unit
) {
val isSignOutPromptCancellable = remainingAttempts > 0
val isSignOutPromptCancellable = (remainingAttempts.dataOrNull() ?: 0) > 0
}

View File

@@ -18,6 +18,7 @@ package io.element.android.features.lockscreen.impl.unlock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
import io.element.android.libraries.architecture.Async
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
override val values: Sequence<PinUnlockState>
@@ -38,7 +39,7 @@ fun aPinUnlockState(
) = PinUnlockState(
pinEntry = pinEntry,
showWrongPinTitle = showWrongPinTitle,
remainingAttempts = remainingAttempts,
remainingAttempts = Async.Success(remainingAttempts),
showSignOutPrompt = showSignOutPrompt,
eventSink = {}
)

View File

@@ -226,10 +226,15 @@ private fun PinUnlockHeader(
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.height(8.dp))
val subtitle = if (state.showWrongPinTitle) {
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = state.remainingAttempts, state.remainingAttempts)
val remainingAttempts = state.remainingAttempts.dataOrNull()
val subtitle = if (remainingAttempts != null) {
if (state.showWrongPinTitle) {
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts)
} else {
stringResource(id = R.string.screen_app_lock_subtitle)
}
} else {
stringResource(id = R.string.screen_app_lock_subtitle)
""
}
val subtitleColor = if (state.showWrongPinTitle) {
MaterialTheme.colorScheme.error

View File

@@ -40,7 +40,7 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
// False positive lint issue
@SuppressLint("WrongConstant")
override fun getOrCreateKey(alias: String): SecretKey {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).also { it.load(null) }
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
?.secretKey
return if (secretKeyEntry == null) {