From 432e20961821b2433c54573bb22e413f35377f51 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 15:07:05 +0200 Subject: [PATCH] PIN unlock : makes sure to load the pin size from storage --- .../android/appconfig/LockScreenConfig.kt | 2 +- .../impl/components/PinEntryTextField.kt | 10 ++-- .../impl/pin/DefaultPinCodeManager.kt | 9 ++- .../lockscreen/impl/pin/PinCodeManager.kt | 5 ++ .../impl/unlock/PinUnlockPresenter.kt | 56 ++++++++++++++----- .../lockscreen/impl/unlock/PinUnlockState.kt | 2 +- .../impl/unlock/PinUnlockStateProvider.kt | 2 +- .../lockscreen/impl/unlock/PinUnlockView.kt | 10 ++-- .../impl/src/main/res/values/localazy.xml | 5 +- 9 files changed, 73 insertions(+), 28 deletions(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index c809e0c10e..75cf8406ed 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -21,7 +21,7 @@ object LockScreenConfig { /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = true + const val IS_PIN_MANDATORY: Boolean = false /** * Some PINs are blacklisted. diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index 017a926b61..e4869e71d8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -20,7 +20,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -58,16 +59,17 @@ fun PinEntryTextField( ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun PinEntryRow( pinEntry: PinEntry, isSecured: Boolean, modifier: Modifier = Modifier, ) { - Row( + FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { for (digit in pinEntry.digits) { PinDigitView(digit = digit, isSecured = isSecured) @@ -98,7 +100,7 @@ private fun PinDigitView( ) { if (digit is PinDigit.Filled) { - val text = if(isSecured) { + val text = if (isSecured) { "•" } else { digit.value.toString() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index b7b861199e..e5e2dc6106 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -23,8 +23,6 @@ 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 io.element.android.libraries.sessionstorage.api.observer.SessionObserver -import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } + override suspend fun getPinCodeSize(): Int { + val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0 + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) + return decryptedPinCode.size + } + override suspend fun createPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 2f0d44d9f2..c214e533ab 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -57,6 +57,11 @@ interface PinCodeManager { */ suspend fun isPinCodeAvailable(): Boolean + /** + * @return the size of the saved pin code. + */ + suspend fun getPinCodeSize(): Int + /** * Creates a new encrypted pin code. * @param pinCode the clear pin code to create diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index e73650314b..19f075b5b5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -24,13 +24,13 @@ 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.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 io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -44,10 +44,10 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - var pinEntry by remember { - //TODO fetch size from db - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + val pinEntryState = remember { + mutableStateOf>(Async.Uninitialized) } + val pinEntry by pinEntryState var remainingAttempts by remember { mutableStateOf>(Async.Uninitialized) } @@ -62,11 +62,18 @@ class PinUnlockPresenter @Inject constructor( mutableStateOf>(Async.Uninitialized) } + LaunchedEffect(Unit) { + suspend { + val pinCodeSize = pinCodeManager.getPinCodeSize() + PinEntry.createEmpty(pinCodeSize) + }.runCatchingUpdatingState(pinEntryState) + } + LaunchedEffect(pinEntry) { if (pinEntry.isComplete()) { val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText()) if (!isVerified) { - pinEntry = pinEntry.clear() + pinEntryState.value = pinEntry.clear() showWrongPinTitle = true } } @@ -80,7 +87,7 @@ class PinUnlockPresenter @Inject constructor( fun handleEvents(event: PinUnlockEvents) { when (event) { is PinUnlockEvents.OnPinKeypadPressed -> { - pinEntry = pinEntry.process(event.pinKeypadModel) + pinEntryState.value = pinEntry.process(event.pinKeypadModel) } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false @@ -103,17 +110,38 @@ class PinUnlockPresenter @Inject constructor( ) } + private fun Async.isComplete(): Boolean { + return dataOrNull()?.isComplete().orFalse() + } + + private fun Async.toText(): String { + return dataOrNull()?.toText() ?: "" + } + + private fun Async.clear(): Async { + return when (this) { + is Async.Success -> Async.Success(data.clear()) + else -> this + } + } + + private fun Async.process(pinKeypadModel: PinKeypadModel): Async { + return when (this) { + is Async.Success -> { + val pinEntry = when (pinKeypadModel) { + PinKeypadModel.Back -> data.deleteLast() + is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number) + PinKeypadModel.Empty -> data + } + Async.Success(pinEntry) + } + else -> this + } + } + private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { matrixClient.logout() }.runCatchingUpdatingState(signOutAction) } - - private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { - return when (pinKeypadModel) { - PinKeypadModel.Back -> deleteLast() - is PinKeypadModel.Number -> addDigit(pinKeypadModel.number) - PinKeypadModel.Empty -> this - } - } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index fa80254cce..178e8692d1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -20,7 +20,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.libraries.architecture.Async data class PinUnlockState( - val pinEntry: PinEntry, + val pinEntry: Async, val showWrongPinTitle: Boolean, val remainingAttempts: Async, val showSignOutPrompt: Boolean, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index cd2959dfe1..009a5bf4a4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -39,7 +39,7 @@ fun aPinUnlockState( showSignOutPrompt: Boolean = false, signOutAction: Async = Async.Uninitialized, ) = PinUnlockState( - pinEntry = pinEntry, + pinEntry = Async.Success(pinEntry), showWrongPinTitle = showWrongPinTitle, remainingAttempts = Async.Success(remainingAttempts), showSignOutPrompt = showSignOutPrompt, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 00a59e63cc..514531a718 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -258,7 +258,7 @@ private fun PinUnlockHeader( 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) + pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts) } } else { "" @@ -276,14 +276,16 @@ private fun PinUnlockHeader( color = subtitleColor, ) Spacer(Modifier.height(24.dp)) - PinDotsRow(state.pinEntry) + if (state.pinEntry is Async.Success) { + PinDotsRow(state.pinEntry.data) + } } } @Composable private fun PinUnlockFooter( - onUseBiometric: ()->Unit, - onForgotPin: ()->Unit, + onUseBiometric: () -> Unit, + onForgotPin: () -> Unit, modifier: Modifier = Modifier, ) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index 529785b74d..bd6d522d57 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + + "You have %1$d attempt to unlock" + "You have %1$d attempts to unlock" + "Wrong PIN. You have %1$d more chance" "Wrong PIN. You have %1$d more chances" @@ -26,6 +30,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th "PINs don\'t match" "You’ll need to re-login and create a new PIN to proceed" "You are being signed out" - "You have 3 attempts to unlock" "Signing out…"