PIN : start branching logic
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -23,5 +23,4 @@ interface LockScreenStateService {
|
||||
|
||||
suspend fun entersForeground()
|
||||
suspend fun entersBackground()
|
||||
suspend fun unlock()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ fun aLockScreenSettingsState(
|
||||
isBiometricEnabled: Boolean = false,
|
||||
showRemovePinConfirmation: Boolean = false,
|
||||
) = LockScreenSettingsState(
|
||||
isPinMandatory = isLockMandatory,
|
||||
showRemovePinOption = isLockMandatory,
|
||||
isBiometricEnabled = isBiometricEnabled,
|
||||
showRemovePinConfirmation = showRemovePinConfirmation,
|
||||
eventSink = {}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user