PIN : start branching logic
This commit is contained in:
parent
b0f27c111e
commit
645c699a6b
20 changed files with 316 additions and 78 deletions
|
|
@ -32,4 +32,9 @@ object LockScreenConfig {
|
||||||
* The size of the PIN.
|
* The size of the PIN.
|
||||||
*/
|
*/
|
||||||
const val PIN_SIZE = 4
|
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 entersForeground()
|
||||||
suspend fun entersBackground()
|
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.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
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.settings.LockScreenSettingsNode
|
||||||
import io.element.android.features.lockscreen.impl.setup.SetupPinNode
|
import io.element.android.features.lockscreen.impl.setup.SetupPinNode
|
||||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
|
import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode
|
||||||
|
|
@ -74,7 +76,7 @@ class LockScreenFlowNode @AssistedInject constructor(
|
||||||
createNode<SetupPinNode>(buildContext)
|
createNode<SetupPinNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.Settings -> {
|
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.EncryptionResult
|
||||||
import io.element.android.libraries.cryptography.api.SecretKeyProvider
|
import io.element.android.libraries.cryptography.api.SecretKeyProvider
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.inject.Inject
|
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)
|
@ContributesBinding(AppScope::class)
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class DefaultPinCodeManager @Inject constructor(
|
class DefaultPinCodeManager @Inject constructor(
|
||||||
private val secretKeyProvider: SecretKeyProvider,
|
private val secretKeyProvider: SecretKeyProvider,
|
||||||
private val encryptionDecryptionService: EncryptionDecryptionService,
|
private val encryptionDecryptionService: EncryptionDecryptionService,
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
) : PinCodeManager {
|
) : 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 {
|
override suspend fun isPinCodeAvailable(): Boolean {
|
||||||
return pinCodeStore.hasPinCode()
|
return pinCodeStore.hasPinCode()
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +54,7 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
||||||
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
|
val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64()
|
||||||
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
|
pinCodeStore.saveEncryptedPinCode(encryptedPinCode)
|
||||||
|
callbacks.forEach { it.onPinCodeCreated() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun verifyPinCode(pinCode: String): Boolean {
|
override suspend fun verifyPinCode(pinCode: String): Boolean {
|
||||||
|
|
@ -48,7 +62,17 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
return try {
|
return try {
|
||||||
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
||||||
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
|
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) {
|
} catch (failure: Throwable) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -56,17 +80,11 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
|
|
||||||
override suspend fun deletePinCode() {
|
override suspend fun deletePinCode() {
|
||||||
pinCodeStore.deleteEncryptedPinCode()
|
pinCodeStore.deleteEncryptedPinCode()
|
||||||
|
pinCodeStore.resetCounter()
|
||||||
|
callbacks.forEach { it.onPinCodeRemoved() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||||
return pinCodeStore.getRemainingPinCodeAttemptsNumber()
|
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.
|
* Implementation should take care of encrypting the pin code and storing it.
|
||||||
*/
|
*/
|
||||||
interface PinCodeManager {
|
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.
|
* @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.
|
* @return the number of remaining attempts before the pin code is blocked.
|
||||||
*/
|
*/
|
||||||
suspend fun getRemainingPinCodeAttemptsNumber(): Int
|
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 android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.DefaultPreferences
|
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 ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY"
|
||||||
private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_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)
|
@SingleIn(AppScope::class)
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
|
|
@ -57,8 +57,6 @@ class SharedPreferencesPinCodeStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) {
|
override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) {
|
||||||
// Also reset the counters
|
|
||||||
resetCounter()
|
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
remove(ENCODED_PIN_CODE_KEY)
|
remove(ENCODED_PIN_CODE_KEY)
|
||||||
}
|
}
|
||||||
|
|
@ -72,14 +70,12 @@ class SharedPreferencesPinCodeStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) {
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) {
|
||||||
mutex.withLock {
|
sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
|
||||||
sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onWrongPin(): Int = withContext(dispatchers.io) {
|
override suspend fun onWrongPin(): Int = withContext(dispatchers.io) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val remaining = getRemainingPinCodeAttemptsNumber() - 1
|
val remaining = (getRemainingPinCodeAttemptsNumber() - 1).coerceAtLeast(0)
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining)
|
putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,5 @@ sealed interface LockScreenSettingsEvents {
|
||||||
data object RemovePin : LockScreenSettingsEvents
|
data object RemovePin : LockScreenSettingsEvents
|
||||||
data object ConfirmRemovePin : LockScreenSettingsEvents
|
data object ConfirmRemovePin : LockScreenSettingsEvents
|
||||||
data object CancelRemovePin : LockScreenSettingsEvents
|
data object CancelRemovePin : LockScreenSettingsEvents
|
||||||
data object ChangePin : LockScreenSettingsEvents
|
|
||||||
data object ToggleBiometric : 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.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.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
|
@ -33,12 +34,22 @@ class LockScreenSettingsNode @AssistedInject constructor(
|
||||||
private val presenter: LockScreenSettingsPresenter,
|
private val presenter: LockScreenSettingsPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onChangePinClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onChangePinClicked() {
|
||||||
|
plugins<Callback>().forEach { it.onChangePinClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
LockScreenSettingsView(
|
LockScreenSettingsView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier
|
onBackPressed = this::navigateUp,
|
||||||
|
onChangePinClicked = this::onChangePinClicked,
|
||||||
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,41 +17,65 @@
|
||||||
package io.element.android.features.lockscreen.impl.settings
|
package io.element.android.features.lockscreen.impl.settings
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
import io.element.android.appconfig.LockScreenConfig
|
||||||
|
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
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
|
@Composable
|
||||||
override fun present(): LockScreenSettingsState {
|
override fun present(): LockScreenSettingsState {
|
||||||
|
var triggerComputation by remember {
|
||||||
|
mutableIntStateOf(0)
|
||||||
|
}
|
||||||
|
var showRemovePinOption by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
var isBiometricEnabled by remember {
|
var isBiometricEnabled by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
var showRemovePinConfirmation by remember {
|
var showRemovePinConfirmation by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(triggerComputation) {
|
||||||
|
showRemovePinOption = !LockScreenConfig.IS_PIN_MANDATORY && pinCodeManager.isPinCodeAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: LockScreenSettingsEvents) {
|
fun handleEvents(event: LockScreenSettingsEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
LockScreenSettingsEvents.CancelRemovePin -> TODO()
|
LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false
|
||||||
LockScreenSettingsEvents.ChangePin -> TODO()
|
LockScreenSettingsEvents.ConfirmRemovePin -> {
|
||||||
LockScreenSettingsEvents.ConfirmRemovePin -> TODO()
|
coroutineScope.launch {
|
||||||
LockScreenSettingsEvents.RemovePin -> TODO()
|
showRemovePinConfirmation = false
|
||||||
LockScreenSettingsEvents.ToggleBiometric -> TODO()
|
pinCodeManager.deletePinCode()
|
||||||
|
triggerComputation++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LockScreenSettingsEvents.RemovePin -> showRemovePinConfirmation = true
|
||||||
|
LockScreenSettingsEvents.ToggleBiometric -> {
|
||||||
|
//TODO branch biometric logic
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return LockScreenSettingsState(
|
return LockScreenSettingsState(
|
||||||
isPinMandatory = LockScreenConfig.IS_PIN_MANDATORY,
|
showRemovePinOption = showRemovePinOption,
|
||||||
isBiometricEnabled = isBiometricEnabled,
|
isBiometricEnabled = isBiometricEnabled,
|
||||||
showRemovePinConfirmation = showRemovePinConfirmation,
|
showRemovePinConfirmation = showRemovePinConfirmation,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.features.lockscreen.impl.settings
|
package io.element.android.features.lockscreen.impl.settings
|
||||||
|
|
||||||
data class LockScreenSettingsState(
|
data class LockScreenSettingsState(
|
||||||
val isPinMandatory: Boolean,
|
val showRemovePinOption: Boolean,
|
||||||
val isBiometricEnabled: Boolean,
|
val isBiometricEnabled: Boolean,
|
||||||
val showRemovePinConfirmation: Boolean,
|
val showRemovePinConfirmation: Boolean,
|
||||||
val eventSink: (LockScreenSettingsEvents) -> Unit
|
val eventSink: (LockScreenSettingsEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ fun aLockScreenSettingsState(
|
||||||
isBiometricEnabled: Boolean = false,
|
isBiometricEnabled: Boolean = false,
|
||||||
showRemovePinConfirmation: Boolean = false,
|
showRemovePinConfirmation: Boolean = false,
|
||||||
) = LockScreenSettingsState(
|
) = LockScreenSettingsState(
|
||||||
isPinMandatory = isLockMandatory,
|
showRemovePinOption = isLockMandatory,
|
||||||
isBiometricEnabled = isBiometricEnabled,
|
isBiometricEnabled = isBiometricEnabled,
|
||||||
showRemovePinConfirmation = showRemovePinConfirmation,
|
showRemovePinConfirmation = showRemovePinConfirmation,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -34,21 +34,22 @@ import io.element.android.libraries.theme.ElementTheme
|
||||||
@Composable
|
@Composable
|
||||||
fun LockScreenSettingsView(
|
fun LockScreenSettingsView(
|
||||||
state: LockScreenSettingsState,
|
state: LockScreenSettingsState,
|
||||||
|
onChangePinClicked: () -> Unit,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
PreferencePage(
|
PreferencePage(
|
||||||
title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock),
|
title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock),
|
||||||
|
onBackPressed = onBackPressed,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
PreferenceCategory(showDivider = false) {
|
PreferenceCategory(showDivider = false) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
|
title = stringResource(id = R.string.screen_app_lock_settings_change_pin),
|
||||||
onClick = {
|
onClick = onChangePinClicked
|
||||||
state.eventSink(LockScreenSettingsEvents.ChangePin)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
PreferenceDivider()
|
PreferenceDivider()
|
||||||
if (!state.isPinMandatory) {
|
if (state.showRemovePinOption) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
title = stringResource(id = R.string.screen_app_lock_settings_remove_pin),
|
title = stringResource(id = R.string.screen_app_lock_settings_remove_pin),
|
||||||
tintColor = ElementTheme.colors.textCriticalPrimary,
|
tintColor = ElementTheme.colors.textCriticalPrimary,
|
||||||
|
|
@ -80,6 +81,10 @@ internal fun LockScreenSettingsViewPreview(
|
||||||
@PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState,
|
@PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState,
|
||||||
) {
|
) {
|
||||||
ElementPreview {
|
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.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
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.pin.model.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
|
import io.element.android.features.lockscreen.impl.setup.validation.PinValidator
|
||||||
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
|
import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure
|
||||||
|
|
@ -34,6 +35,7 @@ import javax.inject.Inject
|
||||||
class SetupPinPresenter @Inject constructor(
|
class SetupPinPresenter @Inject constructor(
|
||||||
private val pinValidator: PinValidator,
|
private val pinValidator: PinValidator,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
private val pinCodeManager: PinCodeManager,
|
||||||
) : Presenter<SetupPinState> {
|
) : Presenter<SetupPinState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -68,7 +70,7 @@ class SetupPinPresenter @Inject constructor(
|
||||||
LaunchedEffect(confirmPinEntry) {
|
LaunchedEffect(confirmPinEntry) {
|
||||||
if (confirmPinEntry.isComplete()) {
|
if (confirmPinEntry.isComplete()) {
|
||||||
if (confirmPinEntry == choosePinEntry) {
|
if (confirmPinEntry == choosePinEntry) {
|
||||||
//TODO save in db and navigate to next screen
|
pinCodeManager.createPinCode(confirmPinEntry.toText())
|
||||||
} else {
|
} else {
|
||||||
setupPinFailure = SetupPinFailure.PinsDontMatch
|
setupPinFailure = SetupPinFailure.PinsDontMatch
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,15 @@ package io.element.android.features.lockscreen.impl.state
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.lockscreen.api.LockScreenState
|
import io.element.android.features.lockscreen.api.LockScreenState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenStateService
|
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.AppScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -36,6 +39,8 @@ import javax.inject.Inject
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultLockScreenStateService @Inject constructor(
|
class DefaultLockScreenStateService @Inject constructor(
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val pinCodeManager: PinCodeManager,
|
||||||
|
private val coroutineScope: CoroutineScope,
|
||||||
) : LockScreenStateService {
|
) : LockScreenStateService {
|
||||||
|
|
||||||
private val _lockScreenState = MutableStateFlow<LockScreenState>(LockScreenState.Unlocked)
|
private val _lockScreenState = MutableStateFlow<LockScreenState>(LockScreenState.Unlocked)
|
||||||
|
|
@ -43,10 +48,13 @@ class DefaultLockScreenStateService @Inject constructor(
|
||||||
|
|
||||||
private var lockJob: Job? = null
|
private var lockJob: Job? = null
|
||||||
|
|
||||||
override suspend fun unlock() {
|
init {
|
||||||
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) {
|
pinCodeManager.addCallback(object : PinCodeManager.Callback {
|
||||||
_lockScreenState.value = LockScreenState.Unlocked
|
override fun onPinCodeVerified() {
|
||||||
}
|
_lockScreenState.value = LockScreenState.Unlocked
|
||||||
|
}
|
||||||
|
})
|
||||||
|
coroutineScope.lockIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun entersForeground() {
|
override suspend fun entersForeground() {
|
||||||
|
|
@ -54,11 +62,13 @@ class DefaultLockScreenStateService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun entersBackground() = coroutineScope {
|
override suspend fun entersBackground() = coroutineScope {
|
||||||
lockJob = launch {
|
lockJob = lockIfNeeded()
|
||||||
if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) {
|
}
|
||||||
//delay(GRACE_PERIOD_IN_MILLIS)
|
|
||||||
_lockScreenState.value = LockScreenState.Locked
|
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
|
package io.element.android.features.lockscreen.impl.unlock
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.appconfig.LockScreenConfig
|
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.pin.model.PinEntry
|
||||||
import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel
|
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.Presenter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PinUnlockPresenter @Inject constructor(
|
class PinUnlockPresenter @Inject constructor(
|
||||||
private val pinStateService: LockScreenStateService,
|
private val pinCodeManager: PinCodeManager,
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
) : Presenter<PinUnlockState> {
|
) : Presenter<PinUnlockState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -43,9 +41,8 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
//TODO fetch size from db
|
//TODO fetch size from db
|
||||||
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
|
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
|
||||||
}
|
}
|
||||||
var remainingAttempts by rememberSaveable {
|
var remainingAttempts by remember {
|
||||||
//TODO fetch from db
|
mutableStateOf<Async<Int>>(Async.Uninitialized)
|
||||||
mutableIntStateOf(3)
|
|
||||||
}
|
}
|
||||||
var showWrongPinTitle by rememberSaveable {
|
var showWrongPinTitle by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
|
|
@ -54,14 +51,25 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
mutableStateOf(false)
|
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) {
|
fun handleEvents(event: PinUnlockEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PinUnlockEvents.OnPinKeypadPressed -> {
|
is PinUnlockEvents.OnPinKeypadPressed -> {
|
||||||
pinEntry = pinEntry.process(event.pinKeypadModel)
|
pinEntry = pinEntry.process(event.pinKeypadModel)
|
||||||
if (pinEntry.isComplete()) {
|
|
||||||
//TODO check pin with PinCodeManager
|
|
||||||
coroutineScope.launch { pinStateService.unlock() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
||||||
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@
|
||||||
package io.element.android.features.lockscreen.impl.unlock
|
package io.element.android.features.lockscreen.impl.unlock
|
||||||
|
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
data class PinUnlockState(
|
data class PinUnlockState(
|
||||||
val pinEntry: PinEntry,
|
val pinEntry: PinEntry,
|
||||||
val showWrongPinTitle: Boolean,
|
val showWrongPinTitle: Boolean,
|
||||||
val remainingAttempts: Int,
|
val remainingAttempts: Async<Int>,
|
||||||
val showSignOutPrompt: Boolean,
|
val showSignOutPrompt: Boolean,
|
||||||
val eventSink: (PinUnlockEvents) -> Unit
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
open class PinUnlockStateProvider : PreviewParameterProvider<PinUnlockState> {
|
||||||
override val values: Sequence<PinUnlockState>
|
override val values: Sequence<PinUnlockState>
|
||||||
|
|
@ -38,7 +39,7 @@ fun aPinUnlockState(
|
||||||
) = PinUnlockState(
|
) = PinUnlockState(
|
||||||
pinEntry = pinEntry,
|
pinEntry = pinEntry,
|
||||||
showWrongPinTitle = showWrongPinTitle,
|
showWrongPinTitle = showWrongPinTitle,
|
||||||
remainingAttempts = remainingAttempts,
|
remainingAttempts = Async.Success(remainingAttempts),
|
||||||
showSignOutPrompt = showSignOutPrompt,
|
showSignOutPrompt = showSignOutPrompt,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -226,10 +226,15 @@ private fun PinUnlockHeader(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
val subtitle = if (state.showWrongPinTitle) {
|
val remainingAttempts = state.remainingAttempts.dataOrNull()
|
||||||
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = state.remainingAttempts, state.remainingAttempts)
|
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 {
|
} else {
|
||||||
stringResource(id = R.string.screen_app_lock_subtitle)
|
""
|
||||||
}
|
}
|
||||||
val subtitleColor = if (state.showWrongPinTitle) {
|
val subtitleColor = if (state.showWrongPinTitle) {
|
||||||
MaterialTheme.colorScheme.error
|
MaterialTheme.colorScheme.error
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider {
|
||||||
// False positive lint issue
|
// False positive lint issue
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
override fun getOrCreateKey(alias: String): SecretKey {
|
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)
|
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
||||||
?.secretKey
|
?.secretKey
|
||||||
return if (secretKeyEntry == null) {
|
return if (secretKeyEntry == null) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue