diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt new file mode 100644 index 0000000000..9427a1f9c7 --- /dev/null +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -0,0 +1,35 @@ +/* + * 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.appconfig + +object LockScreenConfig { + + /** + * Whether the PIN is mandatory or not. + */ + const val IS_PIN_MANDATORY: Boolean = false + + /** + * Some PINs are blacklisted. + */ + val PIN_BLACKLIST = setOf("0000", "1234") + + /** + * The size of the PIN. + */ + const val PIN_SIZE = 4 +} diff --git a/build.gradle.kts b/build.gradle.kts index e14ad71981..f08c023b1d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -250,9 +250,6 @@ koverMerged { excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" // Some options can't be tested at the moment excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*" - // Temporary until we have actually something to test. - excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter" - excludes += "io.element.android.features.lockscreen.impl.auth.PinAuthenticationPresenter$*" } bound { minValue = 85 diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 028d8bee3c..a3657ccff3 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -30,9 +30,11 @@ anvil { } dependencies { + ksp(libs.showkase.processor) implementation(projects.anvilannotations) anvil(projects.anvilcodegen) api(projects.features.lockscreen.api) + implementation(projects.appconfig) implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) @@ -40,6 +42,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -50,6 +53,5 @@ dependencies { testImplementation(projects.tests.testutils) testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) - - ksp(libs.showkase.processor) + testImplementation(projects.libraries.featureflag.test) } 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 d2989d53cc..fa3b88e18a 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 @@ -27,8 +27,8 @@ import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.lockscreen.impl.auth.PinAuthenticationNode -import io.element.android.features.lockscreen.impl.create.CreatePinNode +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 @@ -41,7 +41,7 @@ class LockScreenFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Auth, + initialElement = NavTarget.Unlock, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -50,19 +50,19 @@ class LockScreenFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - data object Auth : NavTarget + data object Unlock : NavTarget @Parcelize - data object Create : NavTarget + data object Setup : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Auth -> { - createNode(buildContext) + NavTarget.Unlock -> { + createNode(buildContext) } - NavTarget.Create -> { - createNode(buildContext) + NavTarget.Setup -> { + createNode(buildContext) } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt deleted file mode 100644 index ecc82f421c..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationPresenter.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.auth - -import androidx.compose.runtime.Composable -import io.element.android.features.lockscreen.api.LockScreenStateService -import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class PinAuthenticationPresenter @Inject constructor( - private val pinStateService: LockScreenStateService, - private val coroutineScope: CoroutineScope, -) : Presenter { - - @Composable - override fun present(): PinAuthenticationState { - - fun handleEvents(event: PinAuthenticationEvents) { - when (event) { - PinAuthenticationEvents.Unlock -> coroutineScope.launch { pinStateService.unlock() } - } - } - return PinAuthenticationState( - eventSink = ::handleEvents - ) - } -} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt deleted file mode 100644 index a2612ed858..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationStateProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.auth - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -open class PinAuthenticationStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aPinAuthenticationState(), - ) -} - -fun aPinAuthenticationState() = PinAuthenticationState( - eventSink = {} -) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt deleted file mode 100644 index 2b62e46800..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationView.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.auth - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Lock -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.Surface - -@Composable -fun PinAuthenticationView( - state: PinAuthenticationState, - modifier: Modifier = Modifier, -) { - Surface(modifier) { - HeaderFooterPage( - modifier = Modifier - .systemBarsPadding() - .fillMaxSize(), - header = { PinAuthenticationHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) }, - footer = { PinAuthenticationFooter(state) }, - ) - } -} - -@Composable -private fun PinAuthenticationHeader( - modifier: Modifier = Modifier, -) { - IconTitleSubtitleMolecule( - modifier = modifier, - title = "Element X is locked", - subTitle = null, - iconImageVector = Icons.Default.Lock, - ) -} - -@Composable -private fun PinAuthenticationFooter(state: PinAuthenticationState) { - Button( - modifier = Modifier.fillMaxWidth(), - text = "Unlock", - onClick = { - state.eventSink(PinAuthenticationEvents.Unlock) - } - ) -} - -@Composable -@PreviewsDayNight -internal fun PinAuthenticationViewPreview(@PreviewParameter(PinAuthenticationStateProvider::class) state: PinAuthenticationState) { - ElementPreview { - PinAuthenticationView( - state = state, - ) - } -} - diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt new file mode 100644 index 0000000000..91f6d435c5 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt @@ -0,0 +1,117 @@ +/* + * 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.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.pinDigitBg +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun PinEntryTextField( + pinEntry: PinEntry, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(pinEntry.toText()), + onValueChange = { + onValueChange(it.text) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + decorationBox = { + PinEntryRow(pinEntry = pinEntry) + } + ) +} + +@Composable +private fun PinEntryRow( + pinEntry: PinEntry, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + for (digit in pinEntry.digits) { + PinDigitView(digit = digit) + } + } +} + +@Composable +private fun PinDigitView( + digit: PinDigit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(8.dp) + val appearanceModifier = when (digit) { + PinDigit.Empty -> { + Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) + } + is PinDigit.Filled -> { + Modifier.background(ElementTheme.colors.pinDigitBg, shape) + } + } + Box( + modifier = modifier + .size(48.dp) + .then(appearanceModifier), + contentAlignment = Alignment.Center, + + ) { + if (digit is PinDigit.Filled) { + Text( + text = digit.toText(), + style = ElementTheme.typography.fontHeadingMdBold + ) + } + + } +} + +@PreviewsDayNight +@Composable +internal fun PinEntryTextFieldPreview() { + ElementPreview { + PinEntryTextField( + pinEntry = PinEntry.createEmpty(4).fillWith("12"), + onValueChange = {}, + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt deleted file mode 100644 index c9dcce018d..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinStateProvider.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.create - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure - -open class CreatePinStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aCreatePinState(), - aCreatePinState( - choosePinEntry = PinEntry.empty(4).fillWith("12") - ), - aCreatePinState( - choosePinEntry = PinEntry.empty(4).fillWith("1789"), - isConfirmationStep = true, - ), - aCreatePinState( - choosePinEntry = PinEntry.empty(4).fillWith("1789"), - confirmPinEntry = PinEntry.empty(4).fillWith("1788"), - isConfirmationStep = true, - creationFailure = CreatePinFailure.PinsDontMatch - ), - aCreatePinState( - choosePinEntry = PinEntry.empty(4).fillWith("1111"), - creationFailure = CreatePinFailure.PinBlacklisted - ), - - ) -} - -fun aCreatePinState( - choosePinEntry: PinEntry = PinEntry.empty(4), - confirmPinEntry: PinEntry = PinEntry.empty(4), - isConfirmationStep: Boolean = false, - creationFailure: CreatePinFailure? = null, -) = CreatePinState( - choosePinEntry = choosePinEntry, - confirmPinEntry = confirmPinEntry, - isConfirmationStep = isConfirmationStep, - createPinFailure = creationFailure, - appName = "Element", - eventSink = {} -) - diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt similarity index 92% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt index 741a61cafe..aa3c45e02e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinDigit.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinDigit.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.model +package io.element.android.features.lockscreen.impl.pin.model sealed interface PinDigit { data object Empty : PinDigit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt similarity index 64% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index a97315f2e8..eaca592de9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.model +package io.element.android.features.lockscreen.impl.pin.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -24,7 +24,7 @@ data class PinEntry( ) { companion object { - fun empty(size: Int): PinEntry { + fun createEmpty(size: Int): PinEntry { val digits = List(size) { PinDigit.Empty } return PinEntry( digits = digits.toPersistentList() @@ -50,14 +50,36 @@ data class PinEntry( return copy(digits = newDigits.toPersistentList()) } - fun clear(): PinEntry { - return fillWith("") + fun deleteLast(): PinEntry { + if (isEmpty()) return this + val newDigits = digits.toMutableList() + newDigits.indexOfLast { it is PinDigit.Filled }.also { lastFilled -> + newDigits[lastFilled] = PinDigit.Empty + } + return copy(digits = newDigits.toPersistentList()) } - fun isPinComplete(): Boolean { + fun addDigit(digit: Char): PinEntry { + if (isComplete()) return this + val newDigits = digits.toMutableList() + newDigits.indexOfFirst { it is PinDigit.Empty }.also { firstEmpty -> + newDigits[firstEmpty] = PinDigit.Filled(digit) + } + return copy(digits = newDigits.toPersistentList()) + } + + fun clear(): PinEntry { + return createEmpty(size) + } + + fun isComplete(): Boolean { return digits.all { it is PinDigit.Filled } } + fun isEmpty(): Boolean { + return digits.all { it is PinDigit.Empty } + } + fun toText(): String { return digits.joinToString("") { it.toText() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt similarity index 73% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt index 8c0cb78921..45c5b034b0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/CreatePinFailure.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.validation +package io.element.android.features.lockscreen.impl.setup -sealed interface CreatePinFailure { - data object PinBlacklisted : CreatePinFailure - data object PinsDontMatch : CreatePinFailure +sealed interface SetupPinEvents { + data class OnPinEntryChanged(val entryAsText: String) : SetupPinEvents + data object ClearFailure : SetupPinEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt similarity index 88% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt index 331d6ada84..7474289f1e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class CreatePinNode @AssistedInject constructor( +class SetupPinNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: CreatePinPresenter, + private val presenter: SetupPinPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - CreatePinView( + SetupPinView( state = state, onBackClicked = { }, modifier = modifier diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt similarity index 61% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 957594c0f7..3c380e6be7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -14,88 +14,87 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure -import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.appconfig.LockScreenConfig +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 import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import javax.inject.Inject -private const val PIN_SIZE = 4 - -class CreatePinPresenter @Inject constructor( +class SetupPinPresenter @Inject constructor( private val pinValidator: PinValidator, private val buildMeta: BuildMeta, -) : Presenter { +) : Presenter { @Composable - override fun present(): CreatePinState { + override fun present(): SetupPinState { var choosePinEntry by remember { - mutableStateOf(PinEntry.empty(PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var confirmPinEntry by remember { - mutableStateOf(PinEntry.empty(PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } var isConfirmationStep by remember { mutableStateOf(false) } - var createPinFailure by remember { - mutableStateOf(null) + var setupPinFailure by remember { + mutableStateOf(null) } - fun handleEvents(event: CreatePinEvents) { + fun handleEvents(event: SetupPinEvents) { when (event) { - is CreatePinEvents.OnPinEntryChanged -> { + is SetupPinEvents.OnPinEntryChanged -> { if (isConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) - if (confirmPinEntry.isPinComplete()) { + if (confirmPinEntry.isComplete()) { if (confirmPinEntry == choosePinEntry) { //TODO save in db and navigate to next screen } else { - createPinFailure = CreatePinFailure.PinsDontMatch + setupPinFailure = SetupPinFailure.PinsDontMatch } } } else { choosePinEntry = choosePinEntry.fillWith(event.entryAsText) - if (choosePinEntry.isPinComplete()) { + if (choosePinEntry.isComplete()) { when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { is PinValidator.Result.Invalid -> { - createPinFailure = pinValidationResult.failure + setupPinFailure = pinValidationResult.failure } PinValidator.Result.Valid -> isConfirmationStep = true } } } } - CreatePinEvents.ClearFailure -> { - when (createPinFailure) { - is CreatePinFailure.PinsDontMatch -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) - confirmPinEntry = PinEntry.empty(PIN_SIZE) + SetupPinEvents.ClearFailure -> { + when (setupPinFailure) { + is SetupPinFailure.PinsDontMatch -> { + choosePinEntry = choosePinEntry.clear() + confirmPinEntry = confirmPinEntry.clear() } - is CreatePinFailure.PinBlacklisted -> { - choosePinEntry = PinEntry.empty(PIN_SIZE) + is SetupPinFailure.PinBlacklisted -> { + choosePinEntry = choosePinEntry.clear() } null -> Unit } isConfirmationStep = false - createPinFailure = null + setupPinFailure = null } } } - return CreatePinState( + return SetupPinState( choosePinEntry = choosePinEntry, confirmPinEntry = confirmPinEntry, isConfirmationStep = isConfirmationStep, - createPinFailure = createPinFailure, + setupPinFailure = setupPinFailure, appName = buildMeta.applicationName, eventSink = ::handleEvents ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt similarity index 69% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt index 020076a2ab..3ae4a2c85b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinState.kt @@ -14,20 +14,19 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure -data class CreatePinState( +data class SetupPinState( val choosePinEntry: PinEntry, val confirmPinEntry: PinEntry, val isConfirmationStep: Boolean, - val createPinFailure: CreatePinFailure?, + val setupPinFailure: SetupPinFailure?, val appName: String, - val eventSink: (CreatePinEvents) -> Unit + val eventSink: (SetupPinEvents) -> Unit ) { - val pinSize = choosePinEntry.size val activePinEntry = if (isConfirmationStep) { confirmPinEntry } else { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt new file mode 100644 index 0000000000..bb0a46d10c --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinStateProvider.kt @@ -0,0 +1,61 @@ +/* + * 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.setup + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure + +open class SetupPinStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSetupPinState(), + aSetupPinState( + choosePinEntry = PinEntry.createEmpty(4).fillWith("12") + ), + aSetupPinState( + choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"), + isConfirmationStep = true, + ), + aSetupPinState( + choosePinEntry = PinEntry.createEmpty(4).fillWith("1789"), + confirmPinEntry = PinEntry.createEmpty(4).fillWith("1788"), + isConfirmationStep = true, + creationFailure = SetupPinFailure.PinsDontMatch + ), + aSetupPinState( + choosePinEntry = PinEntry.createEmpty(4).fillWith("1111"), + creationFailure = SetupPinFailure.PinBlacklisted + ), + + ) +} + +fun aSetupPinState( + choosePinEntry: PinEntry = PinEntry.createEmpty(4), + confirmPinEntry: PinEntry = PinEntry.createEmpty(4), + isConfirmationStep: Boolean = false, + creationFailure: SetupPinFailure? = null, +) = SetupPinState( + choosePinEntry = choosePinEntry, + confirmPinEntry = confirmPinEntry, + isConfirmationStep = isConfirmationStep, + setupPinFailure = creationFailure, + appName = "Element", + eventSink = {} +) + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt similarity index 50% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index 063d65f41f..b8f40b06d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -16,23 +16,14 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -41,28 +32,22 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.lockscreen.impl.R -import io.element.android.features.lockscreen.impl.create.model.PinDigit -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure +import io.element.android.features.lockscreen.impl.components.PinEntryTextField +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.theme.pinDigitBg -import io.element.android.libraries.theme.ElementTheme @Composable -fun CreatePinView( - state: CreatePinState, +fun SetupPinView( + state: SetupPinState, onBackClicked: () -> Unit, modifier: Modifier = Modifier, ) { @@ -86,15 +71,15 @@ fun CreatePinView( .verticalScroll(state = scrollState) .padding(vertical = 16.dp, horizontal = 20.dp), ) { - CreatePinHeader(state.isConfirmationStep, state.appName) - CreatePinContent(state) + SetupPinHeader(state.isConfirmationStep, state.appName) + SetupPinContent(state) } } ) } @Composable -private fun CreatePinHeader( +private fun SetupPinHeader( isValidationStep: Boolean, appName: String, modifier: Modifier = Modifier, @@ -116,118 +101,52 @@ private fun CreatePinHeader( } @Composable -private fun CreatePinContent( - state: CreatePinState, +private fun SetupPinContent( + state: SetupPinState, modifier: Modifier = Modifier, ) { PinEntryTextField( state.activePinEntry, onValueChange = { - state.eventSink(CreatePinEvents.OnPinEntryChanged(it)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(it)) }, modifier = modifier .padding(top = 36.dp) .fillMaxWidth() ) - if (state.createPinFailure != null) { + if (state.setupPinFailure != null) { ErrorDialog( modifier = modifier, - title = state.createPinFailure.title(), - content = state.createPinFailure.content(), + title = state.setupPinFailure.title(), + content = state.setupPinFailure.content(), onDismiss = { - state.eventSink(CreatePinEvents.ClearFailure) + state.eventSink(SetupPinEvents.ClearFailure) } ) } } @Composable -private fun CreatePinFailure.content(): String { +private fun SetupPinFailure.content(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) - CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) + SetupPinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_content) + SetupPinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_content) } } @Composable -private fun CreatePinFailure.title(): String { +private fun SetupPinFailure.title(): String { return when (this) { - CreatePinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) - CreatePinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) - } -} - -@Composable -private fun PinEntryTextField( - pinEntry: PinEntry, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, -) { - BasicTextField( - modifier = modifier, - value = TextFieldValue(pinEntry.toText()), - onValueChange = { - onValueChange(it.text) - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - decorationBox = { - PinEntryRow(pinEntry = pinEntry) - } - ) -} - -@Composable -private fun PinEntryRow( - pinEntry: PinEntry, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - ) { - for (digit in pinEntry.digits) { - PinDigitView(digit = digit) - } - } -} - -@Composable -private fun PinDigitView( - digit: PinDigit, - modifier: Modifier = Modifier, -) { - val shape = RoundedCornerShape(8.dp) - val appearanceModifier = when (digit) { - PinDigit.Empty -> { - Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape) - } - is PinDigit.Filled -> { - Modifier.background(ElementTheme.colors.pinDigitBg, shape) - } - } - Box( - modifier = modifier - .size(48.dp) - .then(appearanceModifier), - contentAlignment = Alignment.Center, - - ) { - if (digit is PinDigit.Filled) { - Text( - text = digit.toText(), - style = ElementTheme.typography.fontHeadingMdBold - ) - } - + SetupPinFailure.PinBlacklisted -> stringResource(id = R.string.screen_app_lock_setup_pin_blacklisted_dialog_title) + SetupPinFailure.PinsDontMatch -> stringResource(id = R.string.screen_app_lock_setup_pin_mismatch_dialog_title) } } @Composable @PreviewsDayNight -internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) { +internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) { ElementPreview { - CreatePinView( + SetupPinView( state = state, onBackClicked = {}, ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt similarity index 62% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index 7353ec47d0..b164ee8c88 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -14,29 +14,27 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create.validation +package io.element.android.features.lockscreen.impl.setup.validation -import androidx.annotation.VisibleForTesting -import io.element.android.features.lockscreen.impl.create.model.PinEntry +import io.element.android.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject -class PinValidator @Inject constructor() { +class PinValidator internal constructor(private val pinBlacklist: Set) { - companion object { - @VisibleForTesting - val BLACKLIST = listOf("0000", "1234") - } + @Inject + constructor() : this(LockScreenConfig.PIN_BLACKLIST) sealed interface Result { data object Valid : Result - data class Invalid(val failure: CreatePinFailure) : Result + data class Invalid(val failure: SetupPinFailure) : Result } fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = BLACKLIST.any { it == pinAsText } + val isBlacklisted = pinBlacklist.any { it == pinAsText } return if (isBlacklisted) { - Result.Invalid(CreatePinFailure.PinBlacklisted) + Result.Invalid(SetupPinFailure.PinBlacklisted) } else { Result.Valid } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt similarity index 75% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt index f9f46c430a..3bb21cb9e6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/SetupPinFailure.kt @@ -14,8 +14,9 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.setup.validation -sealed interface PinAuthenticationEvents { - data object Unlock : PinAuthenticationEvents +sealed interface SetupPinFailure { + data object PinBlacklisted : SetupPinFailure + data object PinsDontMatch : SetupPinFailure } 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 dbfeca2c6a..f2e037b111 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 @@ -25,13 +25,12 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags 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 import javax.inject.Inject -private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L +//private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) @@ -57,7 +56,7 @@ class DefaultLockScreenStateService @Inject constructor( override suspend fun entersBackground() = coroutineScope { lockJob = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - delay(GRACE_PERIOD_IN_MILLIS) + //delay(GRACE_PERIOD_IN_MILLIS) _lockScreenState.value = LockScreenState.Locked } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt new file mode 100644 index 0000000000..30ee16df02 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt @@ -0,0 +1,25 @@ +/* + * 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.unlock + +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel + +sealed interface PinUnlockEvents { + data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents + data object OnForgetPin : PinUnlockEvents + data object ClearSignOutPrompt : PinUnlockEvents +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt similarity index 86% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index d236d40cf1..0fba55c17b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -27,16 +27,16 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class PinAuthenticationNode @AssistedInject constructor( +class PinUnlockNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: PinAuthenticationPresenter, + private val presenter: PinUnlockPresenter, ) : Node(buildContext, plugins = plugins) { @Composable override fun View(modifier: Modifier) { val state = presenter.present() - PinAuthenticationView( + PinUnlockView( state = state, modifier = modifier ) 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 new file mode 100644 index 0000000000..e189a2ab39 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -0,0 +1,86 @@ +/* + * 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.unlock + +import androidx.compose.runtime.Composable +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.model.PinEntry +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel +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, +) : Presenter { + + @Composable + override fun present(): PinUnlockState { + var pinEntry by remember { + //TODO fetch size from db + mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + } + var remainingAttempts by rememberSaveable { + //TODO fetch from db + mutableIntStateOf(3) + } + var showWrongPinTitle by rememberSaveable { + mutableStateOf(false) + } + var showSignOutPrompt by rememberSaveable { + mutableStateOf(false) + } + + 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 + } + } + return PinUnlockState( + pinEntry = pinEntry, + showWrongPinTitle = showWrongPinTitle, + remainingAttempts = remainingAttempts, + showSignOutPrompt = showSignOutPrompt, + eventSink = ::handleEvents + ) + } + + private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { + return when (pinKeypadModel) { + PinKeypadModel.Back -> deleteLast() + is PinKeypadModel.Number -> addDigit(pinKeypadModel.number) + PinKeypadModel.Empty -> this + } + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt new file mode 100644 index 0000000000..1787fb8e8b --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -0,0 +1,29 @@ +/* + * 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.unlock + +import io.element.android.features.lockscreen.impl.pin.model.PinEntry + +data class PinUnlockState( + val pinEntry: PinEntry, + val showWrongPinTitle: Boolean, + val remainingAttempts: Int, + val showSignOutPrompt: Boolean, + val eventSink: (PinUnlockEvents) -> Unit +) { + val isSignOutPromptCancellable = remainingAttempts > 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 new file mode 100644 index 0000000000..8ddc942e25 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -0,0 +1,44 @@ +/* + * 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.unlock + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.lockscreen.impl.pin.model.PinEntry + +open class PinUnlockStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPinUnlockState(), + aPinUnlockState(pinEntry = PinEntry.createEmpty(4).fillWith("12")), + aPinUnlockState(showWrongPinTitle = true), + aPinUnlockState(showSignOutPrompt = true), + aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), + ) +} + +fun aPinUnlockState( + pinEntry: PinEntry = PinEntry.createEmpty(4), + remainingAttempts: Int = 3, + showWrongPinTitle: Boolean = false, + showSignOutPrompt: Boolean = false, +) = PinUnlockState( + pinEntry = pinEntry, + showWrongPinTitle = showWrongPinTitle, + remainingAttempts = 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 new file mode 100644 index 0000000000..5769f42b35 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -0,0 +1,270 @@ +/* + * 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.unlock + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.lockscreen.impl.R +import io.element.android.features.lockscreen.impl.pin.model.PinDigit +import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypad +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun PinUnlockView( + state: PinUnlockState, + modifier: Modifier = Modifier, +) { + Surface(modifier) { + BoxWithConstraints { + val commonModifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(all = 20.dp) + + val header = @Composable { + PinUnlockHeader( + state = state, + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp) + ) + } + val footer = @Composable { + PinUnlockFooter( + modifier = Modifier.padding(top = 24.dp) + ) + } + val content = @Composable { constraints: BoxWithConstraintsScope -> + PinKeypad( + onClick = { + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(it)) + }, + maxWidth = constraints.maxWidth, + maxHeight = constraints.maxHeight, + horizontalAlignment = Alignment.CenterHorizontally, + ) + } + if (maxHeight < 600.dp) { + PinUnlockCompactView( + header = header, + footer = footer, + content = content, + modifier = commonModifier, + ) + } else { + PinUnlockExpandedView( + header = header, + footer = footer, + content = content, + modifier = commonModifier, + ) + } + if (state.showSignOutPrompt) { + if (state.isSignOutPromptCancellable) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onSubmitClicked = {}, + onDismiss = {}, + ) + } else { + ErrorDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onDismiss = {}, + ) + } + } + } + } +} + +@Composable +private fun PinUnlockCompactView( + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxWithConstraintsScope.() -> Unit, +) { + Row(modifier = modifier) { + Column(Modifier.weight(1f)) { + header() + Spacer(modifier = Modifier.height(24.dp)) + footer() + } + BoxWithConstraints( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + content() + } + } +} + +@Composable +private fun PinUnlockExpandedView( + header: @Composable () -> Unit, + footer: @Composable () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxWithConstraintsScope.() -> Unit, +) { + Column( + modifier = modifier, + ) { + header() + BoxWithConstraints( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(top = 40.dp), + ) { + content() + } + footer() + } +} + +@Composable +private fun PinDotsRow( + pinEntry: PinEntry, + modifier: Modifier = Modifier, +) { + Row(modifier, horizontalArrangement = spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + for (digit in pinEntry.digits) { + PinDot(isFilled = digit is PinDigit.Filled) + } + } +} + +@Composable +private fun PinDot( + isFilled: Boolean, + modifier: Modifier = Modifier, +) { + val backgroundColor = if (isFilled) { + ElementTheme.colors.iconPrimary + } else { + ElementTheme.colors.bgSubtlePrimary + } + Box( + modifier = modifier + .size(14.dp) + .background(backgroundColor, CircleShape) + ) +} + +@Composable +private fun PinUnlockHeader( + state: PinUnlockState, + modifier: Modifier = Modifier, +) { + Column(modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + modifier = Modifier + .size(32.dp), + tint = ElementTheme.colors.iconPrimary, + imageVector = Icons.Filled.Lock, + contentDescription = "", + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = CommonStrings.common_enter_your_pin), + modifier = Modifier + .fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontHeadingMdBold, + 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) + } else { + stringResource(id = R.string.screen_app_lock_subtitle) + } + val subtitleColor = if (state.showWrongPinTitle) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.secondary + } + Text( + text = subtitle, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = ElementTheme.typography.fontBodyMdRegular, + color = subtitleColor, + ) + Spacer(Modifier.height(24.dp)) + PinDotsRow(state.pinEntry) + } +} + +@Composable +private fun PinUnlockFooter( + modifier: Modifier = Modifier, +) { + Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { + TextButton(text = "Use biometric", onClick = { }) + TextButton(text = stringResource(id = R.string.screen_app_lock_forgot_pin), onClick = { }) + } +} + +@Composable +@PreviewsDayNight +internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { + ElementPreview { + PinUnlockView( + state = state, + ) + } +} + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt new file mode 100644 index 0000000000..9db5cfe11a --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypad.kt @@ -0,0 +1,214 @@ +/* + * 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.unlock.keypad + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Backspace +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.coerceAtMost +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.times +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toSp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.theme.ElementTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +private val spaceBetweenPinKey = 16.dp +private val maxSizePinKey = 80.dp + +@Composable +fun PinKeypad( + onClick: (PinKeypadModel) -> Unit, + maxWidth: Dp, + maxHeight: Dp, + modifier: Modifier = Modifier, + verticalAlignment: Alignment.Vertical = Alignment.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, +) { + val pinKeyMaxWidth = ((maxWidth - 2 * spaceBetweenPinKey) / 3).coerceAtMost(maxSizePinKey) + val pinKeyMaxHeight = ((maxHeight - 3 * spaceBetweenPinKey) / 4).coerceAtMost(maxSizePinKey) + val pinKeySize = if (pinKeyMaxWidth < pinKeyMaxHeight) pinKeyMaxWidth else pinKeyMaxHeight + + val horizontalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterHorizontally) + val verticalArrangement = spacedBy(spaceBetweenPinKey, Alignment.CenterVertically) + Column( + modifier = modifier, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + ) { + PinKeypadRow( + pinKeySize = pinKeySize, + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = persistentListOf(PinKeypadModel.Number('1'), PinKeypadModel.Number('2'), PinKeypadModel.Number('3')), + onClick = onClick, + ) + PinKeypadRow( + pinKeySize = pinKeySize, + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = persistentListOf(PinKeypadModel.Number('4'), PinKeypadModel.Number('5'), PinKeypadModel.Number('6')), + onClick = onClick, + ) + PinKeypadRow( + pinKeySize = pinKeySize, + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = persistentListOf(PinKeypadModel.Number('7'), PinKeypadModel.Number('8'), PinKeypadModel.Number('9')), + onClick = onClick, + ) + PinKeypadRow( + pinKeySize = pinKeySize, + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArrangement, + models = persistentListOf(PinKeypadModel.Empty, PinKeypadModel.Number('0'), PinKeypadModel.Back), + onClick = onClick, + ) + } +} + +@Composable +private fun PinKeypadRow( + models: ImmutableList, + onClick: (PinKeypadModel) -> Unit, + pinKeySize: Dp, + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, +) { + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + modifier = modifier.fillMaxWidth(), + ) { + val commonModifier = Modifier.size(pinKeySize) + for (model in models) { + when (model) { + is PinKeypadModel.Empty -> { + Spacer(modifier = commonModifier) + } + is PinKeypadModel.Back -> { + PinKeypadBackButton( + modifier = commonModifier, + onClick = { onClick(model) }, + ) + } + is PinKeypadModel.Number -> { + PinKeyBadDigitButton( + size = pinKeySize, + modifier = commonModifier, + digit = model.number.toString(), + onClick = { onClick(model) }, + ) + } + } + } + } +} + +@Composable +private fun PinKeypadButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(CircleShape) + .background(color = ElementTheme.colors.bgSubtlePrimary) + .clickable(onClick = onClick), + content = content + ) +} + +@Composable +private fun PinKeyBadDigitButton( + digit: String, + size: Dp, + onClick: (String) -> Unit, + modifier: Modifier = Modifier, +) { + PinKeypadButton( + modifier = modifier, + onClick = { onClick(digit) } + ) { + val fontSize = size.toSp() / 2 + val originalFont = ElementTheme.typography.fontHeadingXlBold + val ratio = fontSize.value / originalFont.fontSize.value + val lineHeight = originalFont.lineHeight * ratio + Text( + text = digit, + color = ElementTheme.colors.textPrimary, + style = originalFont.copy(fontSize = fontSize, lineHeight = lineHeight, letterSpacing = 0.sp), + ) + } +} + +@Composable +private fun PinKeypadBackButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + PinKeypadButton( + modifier = modifier, + onClick = onClick, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Backspace, + contentDescription = null, + ) + } +} + +@Composable +@PreviewsDayNight +internal fun PinKeypadPreview() { + ElementPreview { + BoxWithConstraints { + PinKeypad( + maxWidth = maxWidth, + maxHeight = maxHeight, + onClick = {} + ) + } + } +} + + diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt similarity index 67% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt index 78ce529325..8d232cb21b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/keypad/PinKeypadModel.kt @@ -14,9 +14,13 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.unlock.keypad -sealed interface CreatePinEvents { - data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents - data object ClearFailure : CreatePinEvents +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface PinKeypadModel { + data object Empty : PinKeypadModel + data object Back : PinKeypadModel + data class Number(val number: Char) : PinKeypadModel } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt similarity index 65% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt index 387467534f..37d54677a1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/auth/PinAuthenticationState.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryAssertions.kt @@ -14,8 +14,15 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.auth +package io.element.android.features.lockscreen.impl.pin.model -data class PinAuthenticationState( - val eventSink: (PinAuthenticationEvents) -> Unit -) +import com.google.common.truth.Truth.assertThat + +fun PinEntry.assertText(text: String) { + assertThat(toText()).isEqualTo(text) +} + +fun PinEntry.assertEmpty() { + val isEmpty = digits.all { it is PinDigit.Empty } + assertThat(isEmpty).isTrue() +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt similarity index 61% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 78536bb693..ff797b52f4 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/create/CreatePinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -14,24 +14,24 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.create +package io.element.android.features.lockscreen.impl.setup import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.lockscreen.impl.create.model.PinDigit -import io.element.android.features.lockscreen.impl.create.model.PinEntry -import io.element.android.features.lockscreen.impl.create.validation.CreatePinFailure -import io.element.android.features.lockscreen.impl.create.validation.PinValidator +import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.model.assertText +import io.element.android.features.lockscreen.impl.setup.validation.PinValidator +import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest import org.junit.Test -class CreatePinPresenterTest { +class SetupPinPresenterTest { - private val blacklistedPin = PinValidator.BLACKLIST.first() + private val blacklistedPin = "1234" private val halfCompletePin = "12" private val completePin = "1235" private val mismatchedPin = "1236" @@ -39,58 +39,58 @@ class CreatePinPresenterTest { @Test fun `present - complete flow`() = runTest { - val presenter = createCreatePinPresenter() + val presenter = createSetupPinPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() + assertThat(state.setupPinFailure).isNull() assertThat(state.isConfirmationStep).isFalse() - state.eventSink(CreatePinEvents.OnPinEntryChanged(halfCompletePin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(halfCompletePin)) } awaitItem().also { state -> state.choosePinEntry.assertText(halfCompletePin) state.confirmPinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() + assertThat(state.setupPinFailure).isNull() assertThat(state.isConfirmationStep).isFalse() - state.eventSink(CreatePinEvents.OnPinEntryChanged(blacklistedPin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(blacklistedPin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(blacklistedPin) - assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinBlacklisted) - state.eventSink(CreatePinEvents.ClearFailure) + assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinBlacklisted) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() - assertThat(state.createPinFailure).isNull() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + assertThat(state.setupPinFailure).isNull() + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() - state.eventSink(CreatePinEvents.OnPinEntryChanged(mismatchedPin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(mismatchedPin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(mismatchedPin) - assertThat(state.createPinFailure).isEqualTo(CreatePinFailure.PinsDontMatch) - state.eventSink(CreatePinEvents.ClearFailure) + assertThat(state.setupPinFailure).isEqualTo(SetupPinFailure.PinsDontMatch) + state.eventSink(SetupPinEvents.ClearFailure) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertEmpty() state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isFalse() - assertThat(state.createPinFailure).isNull() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + assertThat(state.setupPinFailure).isNull() + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitLastSequentialItem().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() - state.eventSink(CreatePinEvents.OnPinEntryChanged(completePin)) + state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } awaitItem().also { state -> state.choosePinEntry.assertText(completePin) @@ -99,16 +99,7 @@ class CreatePinPresenterTest { } } - private fun PinEntry.assertText(text: String) { - assertThat(toText()).isEqualTo(text) - } - - private fun PinEntry.assertEmpty() { - val isEmpty = digits.all { it is PinDigit.Empty } - assertThat(isEmpty).isTrue() - } - - private fun createCreatePinPresenter(): CreatePinPresenter { - return CreatePinPresenter(PinValidator(), aBuildMeta()) + private fun createSetupPinPresenter(): SetupPinPresenter { + return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta()) } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt new file mode 100644 index 0000000000..02919edce0 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -0,0 +1,89 @@ +/* + * 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.unlock + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.lockscreen.impl.pin.model.assertEmpty +import io.element.android.features.lockscreen.impl.pin.model.assertText +import io.element.android.features.lockscreen.impl.state.DefaultLockScreenStateService +import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.tests.testutils.awaitLastSequentialItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PinUnlockPresenterTest { + + private val halfCompletePin = "12" + private val completePin = "1235" + + @Test + fun `present - complete flow`() = runTest { + val presenter = createPinUnlockPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + state.pinEntry.assertEmpty() + assertThat(state.showWrongPinTitle).isFalse() + assertThat(state.showSignOutPrompt).isFalse() + assertThat(state.remainingAttempts).isEqualTo(3) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(halfCompletePin) + state.eventSink(PinUnlockEvents.OnForgetPin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showSignOutPrompt).isEqualTo(true) + assertThat(state.isSignOutPromptCancellable).isEqualTo(true) + state.eventSink(PinUnlockEvents.ClearSignOutPrompt) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showSignOutPrompt).isEqualTo(false) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + } + awaitLastSequentialItem().also { state -> + state.pinEntry.assertText(completePin) + } + } + } + + private suspend fun createPinUnlockPresenter(scope: CoroutineScope): PinUnlockPresenter { + val featureFlagService = FakeFeatureFlagService().apply { + setFeatureEnabled(FeatureFlags.PinUnlock, true) + } + val lockScreenStateService = DefaultLockScreenStateService(featureFlagService) + return PinUnlockPresenter( + lockScreenStateService, + scope, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt index 0317a68a00..2ef6c6580c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.R +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.utils.CommonDrawables @@ -61,7 +62,7 @@ fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { @PreviewsDayNight @Composable internal fun TimelineEncryptedHistoryBannerViewPreview() { - ElementTheme { + ElementPreview { TimelineEncryptedHistoryBannerView() } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt index f36e2ca11c..232e9ab0c1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposerLinkDialog.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import io.element.android.libraries.designsystem.components.dialogs.ListDialog import io.element.android.libraries.designsystem.components.list.TextFieldListItem +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text @@ -200,7 +201,7 @@ private fun EditLinkDialog( @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogCreateLinkPreview() { +internal fun TextComposerLinkDialogCreateLinkPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.InsertLink, @@ -212,7 +213,7 @@ internal fun TextComposerLinkDialogCreateLinkPreview() { @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() { +internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.SetLink(null), @@ -224,7 +225,7 @@ internal fun TextComposerLinkDialogCreateLinkWithoutTextPreview() { @PreviewsDayNight @Composable -internal fun TextComposerLinkDialogEditLinkPreview() { +internal fun TextComposerLinkDialogEditLinkPreview() = ElementPreview { TextComposerLinkDialog( onDismissRequest = {}, linkAction = LinkAction.SetLink("https://element.io"), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt index db4f59342c..2fc0420e05 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordingProgress.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @@ -46,9 +47,7 @@ internal fun RecordingProgress( shape = MaterialTheme.shapes.medium, ) .padding(start = 12.dp, end = 20.dp, top = 8.dp, bottom = 8.dp) - .heightIn(26.dp) - - , + .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { Box( @@ -69,6 +68,6 @@ internal fun RecordingProgress( @PreviewsDayNight @Composable -internal fun RecordingProgressPreview() { +internal fun RecordingProgressPreview() = ElementPreview { RecordingProgress() } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 40c73c8eaa..f82f4dac7c 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -35,4 +35,26 @@ class KonsistPreviewTest { it.hasNameEndingWith("DarkPreview").not() } } + + @Test + fun `Functions with '@PreviewsDayNight' annotation should contain 'ElementPreview' composable`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.text.contains("ElementPreview") + } + } + + @Test + fun `Functions with '@PreviewsDayNight' are internal`() { + Konsist + .scopeFromProject() + .functions() + .withAllAnnotationsOf(PreviewsDayNight::class) + .assertTrue { + it.hasInternalModifier + } + } } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f1992899f6..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f19d53c688e3f862894775756f00040adb5cbba99de71c053ed503c1b8af9518 -size 15239 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f7a21c8025..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.auth_null_PinAuthenticationView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11c145595f7713bc7b66f9d07e917bca82d6e06a1b55367801eb4d2cbfef89b0 -size 14353 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2f7ce4747 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de1d44f62edd3f2d30421e49ad435971b165711b8bfe6d4a9475c5fb2f9f83ed +size 8506 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a4c7739624 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.components_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d58d2d25d8f2c07c976bcda0eea7ec101f993ad1ef733fae4c713a67650e33e2 +size 8498 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-D-1_1_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-1_1_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.create_null_CreatePinView-N-1_2_null_4,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-1_2_null_4,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbb5213ce6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-3_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbb48906ee7f1f18034cd7c64766790689f95f4ea4c0310a98430921b3792aee +size 30854 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a018f0927 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ec32849337493132ab88ebc7270728e3c568896e40b0f451b633c924591cbaa +size 28579 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c508164493 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c94f10441359bdb901ee1f8be9c307312586d6ca54495e88ec231c1e931eb36b +size 38994 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..07c06a033c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38456a511fbc72e3d544c8f88138be4f24b6adb3a10629989c037141213a9e19 +size 39441 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a8df974cd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083c3972e5d019ce1d8565a655bd6f6fb6689e17792ddf7ed282d5eee529a7b6 +size 40292 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..02570ec381 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bda49a721fbd19b4cbe0f4dc1b4013154a55e17f78e099f411d1caaef82cfa6c +size 46711 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee9f61a453 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28d2e1634cc0bbf01f6efc2b260625abb26f2105bf1e858f2bd196830d70854c +size 44278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c7864da5cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab8c04689414b5183fa6ab5a479063b478b3d02dfb6bf2df72b1b606c77392ad +size 36278 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5d49a0e78a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fda961a2240c1458ad561c8f4e7fa265bbe6c70cf9b0fbfcd1ae0879036a3343 +size 36714 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b386f26669 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cb889338b21dd8e1a913348ff02cd217f48840c4d54b99ed4d89b67dff771bb +size 37254 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..39854067a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b0011f773767d2806b2269281415f910b4770bc0e95dff2a2a29a2c46f9e458 +size 41488 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb647daed5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d50c34cdf50881d8176d90617c717f7214faeaa4fdce22ec4b93b0e5669b9869 +size 39073