From 645c699a6b668dbd3204583d929df705b46854e2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Oct 2023 14:19:52 +0200 Subject: [PATCH] PIN : start branching logic --- .../android/appconfig/LockScreenConfig.kt | 5 + .../lockscreen/api/LockScreenStateService.kt | 1 - .../lockscreen/impl/LockScreenFlowNode.kt | 4 +- .../impl/pin/DefaultPinCodeManager.kt | 38 +++-- .../lockscreen/impl/pin/PinCodeManager.kt | 43 ++++-- .../storage/SharedPreferencesPinCodeStore.kt | 10 +- .../impl/settings/LockScreenSettingsEvents.kt | 1 - .../settings/LockScreenSettingsFlowNode.kt | 133 ++++++++++++++++++ .../impl/settings/LockScreenSettingsNode.kt | 13 +- .../settings/LockScreenSettingsPresenter.kt | 40 ++++-- .../impl/settings/LockScreenSettingsState.kt | 2 +- .../LockScreenSettingsStateProvider.kt | 2 +- .../impl/settings/LockScreenSettingsView.kt | 15 +- .../impl/setup/SetupPinPresenter.kt | 4 +- .../state/DefaultLockScreenStateService.kt | 28 ++-- .../impl/unlock/PinUnlockPresenter.kt | 34 +++-- .../lockscreen/impl/unlock/PinUnlockState.kt | 5 +- .../impl/unlock/PinUnlockStateProvider.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockView.kt | 11 +- .../impl/KeyStoreSecretKeyProvider.kt | 2 +- 20 files changed, 316 insertions(+), 78 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt 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 9427a1f9c7..5dd903b484 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -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 } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt index 2f2e6b2376..299d7bbc15 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt @@ -23,5 +23,4 @@ interface LockScreenStateService { suspend fun entersForeground() suspend fun entersBackground() - suspend fun unlock() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index 962a133424..b3de0f81ac 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -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(buildContext) } NavTarget.Settings -> { - createNode(buildContext) + createNode(buildContext) } } } 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 e7529e9280..d7674c76a6 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 @@ -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() + + 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() - } } 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 5f84f5296d..2f0d44d9f2 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 @@ -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() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt index 5d53decadf..db84282ebc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -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) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt index 005077d81b..110ca542de 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt @@ -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 } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt new file mode 100644 index 0000000000..2d84db1b7e --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -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, + private val pinCodeManager: PinCodeManager, +) : BackstackNode( + 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(buildContext) + } + NavTarget.Setup -> { + createNode(buildContext) + } + NavTarget.Settings -> { + val callback = object : LockScreenSettingsNode.Callback { + override fun onChangePinClicked() { + backstack.push(NavTarget.Setup) + } + } + createNode(buildContext, plugins = listOf(callback)) + } + NavTarget.Unknown -> node(buildContext) { } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt index 6b47984076..3e19f24e68 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt @@ -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().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, ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index 4ac1d1c863..a91597448a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -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 { +class LockScreenSettingsPresenter @Inject constructor( + private val pinCodeManager: PinCodeManager, + private val coroutineScope: CoroutineScope, +) : Presenter { @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 ) } + } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt index f076c76065..a6697d8a6d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt @@ -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 diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt index 320ec1aa8b..b693b033b1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt @@ -32,7 +32,7 @@ fun aLockScreenSettingsState( isBiometricEnabled: Boolean = false, showRemovePinConfirmation: Boolean = false, ) = LockScreenSettingsState( - isPinMandatory = isLockMandatory, + showRemovePinOption = isLockMandatory, isBiometricEnabled = isBiometricEnabled, showRemovePinConfirmation = showRemovePinConfirmation, eventSink = {} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 55df5632f1..08234b6b07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -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 = {}, + ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 6472b4f1c2..06de31281c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -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 { @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 } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index f2e037b111..601ae1b68a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -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.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 } } } 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 e189a2ab39..c98ef75146 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 @@ -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 { @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.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 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 1787fb8e8b..c6f4dbb34d 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 @@ -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, val showSignOutPrompt: Boolean, val eventSink: (PinUnlockEvents) -> Unit ) { - val isSignOutPromptCancellable = remainingAttempts > 0 + val isSignOutPromptCancellable = (remainingAttempts.dataOrNull() ?: 0) > 0 } 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 8ddc942e25..3358c47fe4 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 @@ -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 { override val values: Sequence @@ -38,7 +39,7 @@ fun aPinUnlockState( ) = PinUnlockState( pinEntry = pinEntry, showWrongPinTitle = showWrongPinTitle, - remainingAttempts = remainingAttempts, + remainingAttempts = Async.Success(remainingAttempts), showSignOutPrompt = showSignOutPrompt, eventSink = {} ) 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 5769f42b35..c87bb00ec8 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 @@ -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 diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt index 2cd09ea8f6..12be896a5b 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -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) {