PIN unlock : makes sure to load the pin size from storage
This commit is contained in:
parent
d59eaab7a8
commit
432e209618
9 changed files with 73 additions and 28 deletions
|
|
@ -21,7 +21,7 @@ object LockScreenConfig {
|
||||||
/**
|
/**
|
||||||
* Whether the PIN is mandatory or not.
|
* Whether the PIN is mandatory or not.
|
||||||
*/
|
*/
|
||||||
const val IS_PIN_MANDATORY: Boolean = true
|
const val IS_PIN_MANDATORY: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some PINs are blacklisted.
|
* Some PINs are blacklisted.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
|
@ -58,16 +59,17 @@ fun PinEntryTextField(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinEntryRow(
|
private fun PinEntryRow(
|
||||||
pinEntry: PinEntry,
|
pinEntry: PinEntry,
|
||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
FlowRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
for (digit in pinEntry.digits) {
|
for (digit in pinEntry.digits) {
|
||||||
PinDigitView(digit = digit, isSecured = isSecured)
|
PinDigitView(digit = digit, isSecured = isSecured)
|
||||||
|
|
@ -98,7 +100,7 @@ private fun PinDigitView(
|
||||||
|
|
||||||
) {
|
) {
|
||||||
if (digit is PinDigit.Filled) {
|
if (digit is PinDigit.Filled) {
|
||||||
val text = if(isSecured) {
|
val text = if (isSecured) {
|
||||||
"•"
|
"•"
|
||||||
} else {
|
} else {
|
||||||
digit.value.toString()
|
digit.value.toString()
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import io.element.android.libraries.cryptography.api.EncryptionResult
|
||||||
import io.element.android.libraries.cryptography.api.SecretKeyProvider
|
import io.element.android.libraries.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 io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor(
|
||||||
return pinCodeStore.hasPinCode()
|
return pinCodeStore.hasPinCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getPinCodeSize(): Int {
|
||||||
|
val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0
|
||||||
|
val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS)
|
||||||
|
val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode))
|
||||||
|
return decryptedPinCode.size
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun createPinCode(pinCode: String) {
|
override suspend fun createPinCode(pinCode: String) {
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ interface PinCodeManager {
|
||||||
*/
|
*/
|
||||||
suspend fun isPinCodeAvailable(): Boolean
|
suspend fun isPinCodeAvailable(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the size of the saved pin code.
|
||||||
|
*/
|
||||||
|
suspend fun getPinCodeSize(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new encrypted pin code.
|
* Creates a new encrypted pin code.
|
||||||
* @param pinCode the clear pin code to create
|
* @param pinCode the clear pin code to create
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,13 @@ 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.features.lockscreen.impl.pin.PinCodeManager
|
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.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -44,10 +44,10 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PinUnlockState {
|
override fun present(): PinUnlockState {
|
||||||
var pinEntry by remember {
|
val pinEntryState = remember {
|
||||||
//TODO fetch size from db
|
mutableStateOf<Async<PinEntry>>(Async.Uninitialized)
|
||||||
mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE))
|
|
||||||
}
|
}
|
||||||
|
val pinEntry by pinEntryState
|
||||||
var remainingAttempts by remember {
|
var remainingAttempts by remember {
|
||||||
mutableStateOf<Async<Int>>(Async.Uninitialized)
|
mutableStateOf<Async<Int>>(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
@ -62,11 +62,18 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
mutableStateOf<Async<String?>>(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
suspend {
|
||||||
|
val pinCodeSize = pinCodeManager.getPinCodeSize()
|
||||||
|
PinEntry.createEmpty(pinCodeSize)
|
||||||
|
}.runCatchingUpdatingState(pinEntryState)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(pinEntry) {
|
LaunchedEffect(pinEntry) {
|
||||||
if (pinEntry.isComplete()) {
|
if (pinEntry.isComplete()) {
|
||||||
val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText())
|
val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText())
|
||||||
if (!isVerified) {
|
if (!isVerified) {
|
||||||
pinEntry = pinEntry.clear()
|
pinEntryState.value = pinEntry.clear()
|
||||||
showWrongPinTitle = true
|
showWrongPinTitle = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +87,7 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
fun handleEvents(event: PinUnlockEvents) {
|
fun handleEvents(event: PinUnlockEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is PinUnlockEvents.OnPinKeypadPressed -> {
|
is PinUnlockEvents.OnPinKeypadPressed -> {
|
||||||
pinEntry = pinEntry.process(event.pinKeypadModel)
|
pinEntryState.value = pinEntry.process(event.pinKeypadModel)
|
||||||
}
|
}
|
||||||
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true
|
||||||
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false
|
||||||
|
|
@ -103,17 +110,38 @@ class PinUnlockPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Async<PinEntry>.isComplete(): Boolean {
|
||||||
|
return dataOrNull()?.isComplete().orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Async<PinEntry>.toText(): String {
|
||||||
|
return dataOrNull()?.toText() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Async<PinEntry>.clear(): Async<PinEntry> {
|
||||||
|
return when (this) {
|
||||||
|
is Async.Success -> Async.Success(data.clear())
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Async<PinEntry>.process(pinKeypadModel: PinKeypadModel): Async<PinEntry> {
|
||||||
|
return when (this) {
|
||||||
|
is Async.Success -> {
|
||||||
|
val pinEntry = when (pinKeypadModel) {
|
||||||
|
PinKeypadModel.Back -> data.deleteLast()
|
||||||
|
is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number)
|
||||||
|
PinKeypadModel.Empty -> data
|
||||||
|
}
|
||||||
|
Async.Success(pinEntry)
|
||||||
|
}
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
|
private fun CoroutineScope.signOut(signOutAction: MutableState<Async<String?>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
matrixClient.logout()
|
matrixClient.logout()
|
||||||
}.runCatchingUpdatingState(signOutAction)
|
}.runCatchingUpdatingState(signOutAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry {
|
|
||||||
return when (pinKeypadModel) {
|
|
||||||
PinKeypadModel.Back -> deleteLast()
|
|
||||||
is PinKeypadModel.Number -> addDigit(pinKeypadModel.number)
|
|
||||||
PinKeypadModel.Empty -> this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
data class PinUnlockState(
|
data class PinUnlockState(
|
||||||
val pinEntry: PinEntry,
|
val pinEntry: Async<PinEntry>,
|
||||||
val showWrongPinTitle: Boolean,
|
val showWrongPinTitle: Boolean,
|
||||||
val remainingAttempts: Async<Int>,
|
val remainingAttempts: Async<Int>,
|
||||||
val showSignOutPrompt: Boolean,
|
val showSignOutPrompt: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ fun aPinUnlockState(
|
||||||
showSignOutPrompt: Boolean = false,
|
showSignOutPrompt: Boolean = false,
|
||||||
signOutAction: Async<String?> = Async.Uninitialized,
|
signOutAction: Async<String?> = Async.Uninitialized,
|
||||||
) = PinUnlockState(
|
) = PinUnlockState(
|
||||||
pinEntry = pinEntry,
|
pinEntry = Async.Success(pinEntry),
|
||||||
showWrongPinTitle = showWrongPinTitle,
|
showWrongPinTitle = showWrongPinTitle,
|
||||||
remainingAttempts = Async.Success(remainingAttempts),
|
remainingAttempts = Async.Success(remainingAttempts),
|
||||||
showSignOutPrompt = showSignOutPrompt,
|
showSignOutPrompt = showSignOutPrompt,
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,7 @@ private fun PinUnlockHeader(
|
||||||
if (state.showWrongPinTitle) {
|
if (state.showWrongPinTitle) {
|
||||||
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts)
|
pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts)
|
||||||
} else {
|
} else {
|
||||||
stringResource(id = R.string.screen_app_lock_subtitle)
|
pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
|
|
@ -276,14 +276,16 @@ private fun PinUnlockHeader(
|
||||||
color = subtitleColor,
|
color = subtitleColor,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
PinDotsRow(state.pinEntry)
|
if (state.pinEntry is Async.Success) {
|
||||||
|
PinDotsRow(state.pinEntry.data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinUnlockFooter(
|
private fun PinUnlockFooter(
|
||||||
onUseBiometric: ()->Unit,
|
onUseBiometric: () -> Unit,
|
||||||
onForgotPin: ()->Unit,
|
onForgotPin: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
|
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<plurals name="screen_app_lock_subtitle">
|
||||||
|
<item quantity="one">"You have %1$d attempt to unlock"</item>
|
||||||
|
<item quantity="other">"You have %1$d attempts to unlock"</item>
|
||||||
|
</plurals>
|
||||||
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
<plurals name="screen_app_lock_subtitle_wrong_pin">
|
||||||
<item quantity="one">"Wrong PIN. You have %1$d more chance"</item>
|
<item quantity="one">"Wrong PIN. You have %1$d more chance"</item>
|
||||||
<item quantity="other">"Wrong PIN. You have %1$d more chances"</item>
|
<item quantity="other">"Wrong PIN. You have %1$d more chances"</item>
|
||||||
|
|
@ -26,6 +30,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th
|
||||||
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
|
<string name="screen_app_lock_setup_pin_mismatch_dialog_title">"PINs don\'t match"</string>
|
||||||
<string name="screen_app_lock_signout_alert_message">"You’ll need to re-login and create a new PIN to proceed"</string>
|
<string name="screen_app_lock_signout_alert_message">"You’ll need to re-login and create a new PIN to proceed"</string>
|
||||||
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
|
<string name="screen_app_lock_signout_alert_title">"You are being signed out"</string>
|
||||||
<string name="screen_app_lock_subtitle">"You have 3 attempts to unlock"</string>
|
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Signing out…"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue