From 4075fcca05bc315c950f1ed1fe3b16d348e31056 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 12:12:53 +0200 Subject: [PATCH 01/22] Lock settings : create UI --- .../impl/settings/LockScreenSettingsEvents.kt | 25 ++++++ .../impl/settings/LockScreenSettingsNode.kt | 44 ++++++++++ .../settings/LockScreenSettingsPresenter.kt | 59 +++++++++++++ .../impl/settings/LockScreenSettingsState.kt | 24 ++++++ .../LockScreenSettingsStateProvider.kt | 39 +++++++++ .../impl/settings/LockScreenSettingsView.kt | 85 +++++++++++++++++++ 6 files changed, 276 insertions(+) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt new file mode 100644 index 0000000000..005077d81b --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.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.settings + +sealed interface LockScreenSettingsEvents { + data object RemovePin : LockScreenSettingsEvents + data object ConfirmRemovePin : LockScreenSettingsEvents + data object CancelRemovePin : LockScreenSettingsEvents + data object ChangePin : LockScreenSettingsEvents + data object ToggleBiometric : LockScreenSettingsEvents +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt new file mode 100644 index 0000000000..6b47984076 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.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.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class LockScreenSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: LockScreenSettingsPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + LockScreenSettingsView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt new file mode 100644 index 0000000000..2f02bb3982 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +import 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.libraries.architecture.Presenter +import javax.inject.Inject + +class LockScreenSettingsPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): LockScreenSettingsState { + + var isLockMandatory by remember { + mutableStateOf(false) + } + var isBiometricEnabled by remember { + mutableStateOf(false) + } + var showRemovePinConfirmation by remember { + mutableStateOf(false) + } + + fun handleEvents(event: LockScreenSettingsEvents) { + when (event) { + LockScreenSettingsEvents.CancelRemovePin -> TODO() + LockScreenSettingsEvents.ChangePin -> TODO() + LockScreenSettingsEvents.ConfirmRemovePin -> TODO() + LockScreenSettingsEvents.RemovePin -> TODO() + LockScreenSettingsEvents.ToggleBiometric -> TODO() + } + } + + return LockScreenSettingsState( + isLockMandatory = isLockMandatory, + isBiometricEnabled = isBiometricEnabled, + showRemovePinConfirmation = showRemovePinConfirmation, + eventSink = ::handleEvents + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt new file mode 100644 index 0000000000..06f74af9c9 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +data class LockScreenSettingsState( + val isLockMandatory: Boolean, + val isBiometricEnabled: Boolean, + val showRemovePinConfirmation: Boolean, + val eventSink: (LockScreenSettingsEvents) -> Unit +) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt new file mode 100644 index 0000000000..3c4ce83452 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class LockScreenSettingsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLockScreenSettingsState(), + aLockScreenSettingsState(isLockMandatory = true), + aLockScreenSettingsState(showRemovePinConfirmation = true), + ) +} + +fun aLockScreenSettingsState( + isLockMandatory: Boolean = false, + isBiometricEnabled: Boolean = false, + showRemovePinConfirmation: Boolean = false, +) = LockScreenSettingsState( + isLockMandatory = isLockMandatory, + isBiometricEnabled = isBiometricEnabled, + showRemovePinConfirmation = showRemovePinConfirmation, + eventSink = {} +) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt new file mode 100644 index 0000000000..57c07213ce --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.lockscreen.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider +import io.element.android.libraries.designsystem.components.preferences.PreferencePage +import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun LockScreenSettingsView( + state: LockScreenSettingsState, + modifier: Modifier = Modifier, +) { + PreferencePage( + title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock), + modifier = modifier + ) { + PreferenceCategory(showDivider = false) { + PreferenceText( + title = stringResource(id = R.string.screen_app_lock_settings_change_pin), + onClick = { + state.eventSink(LockScreenSettingsEvents.ChangePin) + } + ) + PreferenceDivider() + if (!state.isLockMandatory) { + PreferenceText( + title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), + tintColor = ElementTheme.colors.textCriticalPrimary, + onClick = { + state.eventSink(LockScreenSettingsEvents.RemovePin) + } + ) + } + PreferenceDivider() + PreferenceSwitch(title = stringResource(id = R.string.screen_app_lock_settings_enable_biometric_unlock), isChecked = state.isBiometricEnabled) + } + } + if (state.showRemovePinConfirmation) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_title), + content = stringResource(id = R.string.screen_app_lock_settings_remove_pin_alert_message), + onSubmitClicked = { + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + }, + onDismiss = { + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + }) + } +} + +@PreviewsDayNight +@Composable +internal fun LockScreenSettingsViewPreview( + @PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState, +) { + ElementPreview { + LockScreenSettingsView(state) + } +} From afe6d5a870c514f6ee829b30f6cc854248502555 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 18:11:02 +0200 Subject: [PATCH 02/22] Lock settings : branch the flow --- .../android/appnav/LoggedInFlowNode.kt | 8 ++++--- .../lockscreen/api/LockScreenEntryPoint.kt | 20 ++++++++++++++-- .../impl/DefaultLockScreenEntryPoint.kt | 23 +++++++++++++++++-- .../lockscreen/impl/LockScreenFlowNode.kt | 14 ++++++++++- .../settings/LockScreenSettingsPresenter.kt | 6 ++--- .../impl/settings/LockScreenSettingsState.kt | 2 +- .../LockScreenSettingsStateProvider.kt | 2 +- .../impl/settings/LockScreenSettingsView.kt | 2 +- features/preferences/impl/build.gradle.kts | 1 + .../preferences/impl/PreferencesFlowNode.kt | 14 +++++++++++ .../impl/root/PreferencesRootNode.kt | 6 +++++ .../impl/root/PreferencesRootPresenter.kt | 5 ++++ .../impl/root/PreferencesRootState.kt | 1 + .../impl/root/PreferencesRootStateProvider.kt | 1 + .../impl/root/PreferencesRootView.kt | 10 ++++++++ 15 files changed, 100 insertions(+), 15 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 2f7eabb582..754ab167d1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -48,11 +48,11 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint -import io.element.android.features.networkmonitor.api.NetworkMonitor -import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenState import io.element.android.features.lockscreen.api.LockScreenStateService +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -218,7 +218,9 @@ class LoggedInFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.LockPermanent -> { - lockScreenEntryPoint.createNode(this, buildContext) + lockScreenEntryPoint.nodeBuilder(this, buildContext) + .target(LockScreenEntryPoint.Target.Unlock) + .build() } NavTarget.RoomList -> { val callback = object : RoomListEntryPoint.Callback { diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index 3c9aceb2c8..fce3706dce 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -16,6 +16,22 @@ package io.element.android.features.lockscreen.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint -interface LockScreenEntryPoint : SimpleFeatureEntryPoint +interface LockScreenEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun target(target: Target): NodeBuilder + fun build(): Node + } + + enum class Target { + Settings, + Setup, + Unlock + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 736be374cd..289f11113a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -27,7 +27,26 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder { + + var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock + + return object : LockScreenEntryPoint.NodeBuilder { + override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { + innerTarget = target + return this + } + + override fun build(): Node { + val inputs = LockScreenFlowNode.Inputs( + when (innerTarget) { + LockScreenEntryPoint.Target.Unlock -> LockScreenFlowNode.NavTarget.Unlock + LockScreenEntryPoint.Target.Setup -> LockScreenFlowNode.NavTarget.Setup + LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings + } + ) + return parentNode.createNode(buildContext, listOf(inputs)) + } + } } } 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 fa3b88e18a..962a133424 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,9 +27,11 @@ 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.settings.LockScreenSettingsNode 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.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope @@ -41,19 +43,26 @@ class LockScreenFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Unlock, + initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, plugins = plugins, ) { + data class Inputs( + val initialNavTarget: NavTarget = NavTarget.Unlock, + ) : NodeInputs + sealed interface NavTarget : Parcelable { @Parcelize data object Unlock : NavTarget @Parcelize data object Setup : NavTarget + + @Parcelize + data object Settings : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -64,6 +73,9 @@ class LockScreenFlowNode @AssistedInject constructor( NavTarget.Setup -> { createNode(buildContext) } + NavTarget.Settings -> { + createNode(buildContext) + } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index 2f02bb3982..4ac1d1c863 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import io.element.android.appconfig.LockScreenConfig import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -29,9 +30,6 @@ class LockScreenSettingsPresenter @Inject constructor() : Presenter Unit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt index 3c4ce83452..320ec1aa8b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt @@ -32,7 +32,7 @@ fun aLockScreenSettingsState( isBiometricEnabled: Boolean = false, showRemovePinConfirmation: Boolean = false, ) = LockScreenSettingsState( - isLockMandatory = isLockMandatory, + isPinMandatory = isLockMandatory, isBiometricEnabled = isBiometricEnabled, showRemovePinConfirmation = showRemovePinConfirmation, eventSink = {} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 57c07213ce..55df5632f1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -48,7 +48,7 @@ fun LockScreenSettingsView( } ) PreferenceDivider() - if (!state.isLockMandatory) { + if (!state.isPinMandatory) { PreferenceText( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), tintColor = ElementTheme.colors.textCriticalPrimary, diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 4fcc69ff6b..e4ec04c263 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.libraries.mediaupload.api) implementation(projects.libraries.permissions.api) implementation(projects.features.rageshake.api) + implementation(projects.features.lockscreen.api) implementation(projects.features.analytics.api) implementation(projects.features.ftue.api) implementation(projects.features.logout.api) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 6cf0390db2..5144cfeb3f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNode @@ -50,6 +51,7 @@ import kotlinx.parcelize.Parcelize class PreferencesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val lockScreenEntryPoint: LockScreenEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -81,6 +83,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object NotificationSettings : NavTarget + @Parcelize + data object LockScreenSettings : NavTarget + @Parcelize data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget @@ -116,6 +121,10 @@ class PreferencesFlowNode @AssistedInject constructor( backstack.push(NavTarget.NotificationSettings) } + override fun onOpenLockScreenSettings() { + backstack.push(NavTarget.LockScreenSettings) + } + override fun onOpenAdvancedSettings() { backstack.push(NavTarget.AdvancedSettings) } @@ -162,6 +171,11 @@ class PreferencesFlowNode @AssistedInject constructor( val inputs = EditUserProfileNode.Inputs(navTarget.matrixUser) createNode(buildContext, listOf(inputs)) } + NavTarget.LockScreenSettings -> { + lockScreenEntryPoint.nodeBuilder(this, buildContext) + .target(LockScreenEntryPoint.Target.Settings) + .build() + } } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 407832627b..7ea1fa2e8a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -47,6 +47,7 @@ class PreferencesRootNode @AssistedInject constructor( fun onOpenAbout() fun onOpenDeveloperSettings() fun onOpenNotificationSettings() + fun onOpenLockScreenSettings() fun onOpenAdvancedSettings() fun onOpenUserProfile(matrixUser: MatrixUser) } @@ -93,6 +94,10 @@ class PreferencesRootNode @AssistedInject constructor( plugins().forEach { it.onOpenNotificationSettings() } } + private fun onOpenLockScreenSettings() { + plugins().forEach { it.onOpenLockScreenSettings() } + } + private fun onOpenUserProfile(matrixUser: MatrixUser) { plugins().forEach { it.onOpenUserProfile(matrixUser) } } @@ -115,6 +120,7 @@ class PreferencesRootNode @AssistedInject constructor( onSuccessLogout = { onSuccessLogout(activity, it) }, onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) }, onOpenNotificationSettings = this::onOpenNotificationSettings, + onOpenLockScreenSettings = this::onOpenLockScreenSettings, onOpenUserProfile = this::onOpenUserProfile, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 200785e03d..64132db982 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -68,6 +68,10 @@ class PreferencesRootPresenter @Inject constructor( LaunchedEffect(Unit) { showNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings) } + val showLockScreenSettings = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showLockScreenSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) + } // We should display the 'complete verification' option if the current session can be verified val showCompleteVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(false) @@ -95,6 +99,7 @@ class PreferencesRootPresenter @Inject constructor( showAnalyticsSettings = hasAnalyticsProviders, showDeveloperSettings = showDeveloperSettings, showNotificationSettings = showNotificationSettings.value, + showLockScreenSettings = showLockScreenSettings.value, snackbarMessage = snackbarMessage, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index f61ea5890d..d6ff4855de 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -29,6 +29,7 @@ data class PreferencesRootState( val devicesManagementUrl: String?, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, + val showLockScreenSettings: Boolean, val showNotificationSettings: Boolean, val snackbarMessage: SnackbarMessage?, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index 467ca4c6d6..07dd6240bf 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -30,5 +30,6 @@ fun aPreferencesRootState() = PreferencesRootState( showAnalyticsSettings = true, showDeveloperSettings = true, showNotificationSettings = true, + showLockScreenSettings = true, snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete), ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 2999ddea94..34a4890348 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.outlined.InsertChart import androidx.compose.material.icons.outlined.VerifiedUser import androidx.compose.runtime.Composable @@ -53,6 +54,7 @@ fun PreferencesRootView( onManageAccountClicked: (url: String) -> Unit, onOpenAnalytics: () -> Unit, onOpenRageShake: () -> Unit, + onOpenLockScreenSettings: ()->Unit, onOpenAbout: () -> Unit, onOpenDeveloperSettings: () -> Unit, onOpenAdvancedSettings: () -> Unit, @@ -116,6 +118,13 @@ fun PreferencesRootView( iconResourceId = CommonDrawables.ic_compound_info, onClick = onOpenAbout, ) + if (state.showLockScreenSettings) { + PreferenceText( + title = stringResource(id = CommonStrings.common_screen_lock), + icon = Icons.Default.Lock, + onClick = onOpenLockScreenSettings, + ) + } HorizontalDivider() if (state.devicesManagementUrl != null) { PreferenceText( @@ -183,6 +192,7 @@ private fun ContentToPreview(matrixUser: MatrixUser) { onSuccessLogout = {}, onManageAccountClicked = {}, onOpenNotificationSettings = {}, + onOpenLockScreenSettings = {}, onOpenUserProfile = {}, ) } From cf4ac261a9bcf00dc1f95d9dd52b8419fd31666e Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 20:55:58 +0200 Subject: [PATCH 03/22] Fix dagger compilation --- .../impl/pin/storage/SharedPreferencesPinCodeStore.kt | 3 ++- libraries/cryptography/impl/build.gradle.kts | 2 +- plugins/src/main/kotlin/extension/DependencyHandleScope.kt | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt index 27f4636400..5d53decadf 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -21,6 +21,7 @@ import androidx.core.content.edit import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.SingleIn import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -36,7 +37,7 @@ private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 @ContributesBinding(AppScope::class) class SharedPreferencesPinCodeStore @Inject constructor( private val dispatchers: CoroutineDispatchers, - private val sharedPreferences: SharedPreferences, + @DefaultPreferences private val sharedPreferences: SharedPreferences, ) : PinCodeStore { private val listeners = CopyOnWriteArrayList() diff --git a/libraries/cryptography/impl/build.gradle.kts b/libraries/cryptography/impl/build.gradle.kts index 263fecec27..9a6b0a5ce4 100644 --- a/libraries/cryptography/impl/build.gradle.kts +++ b/libraries/cryptography/impl/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(libs.dagger) implementation(projects.anvilannotations) implementation(projects.libraries.di) - implementation(projects.libraries.cryptography.api) + api(projects.libraries.cryptography.api) testImplementation(libs.test.junit) testImplementation(libs.test.truth) diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 1029100823..0d61967236 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -101,6 +101,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:mediaupload:impl")) implementation(project(":libraries:usersearch:impl")) implementation(project(":libraries:textcomposer:impl")) + implementation(project(":libraries:cryptography:impl")) } fun DependencyHandlerScope.allServicesImpl() { From 6ee1c2eb4ca94c3042d1eb19cbf1dd46a82b8853 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 23 Oct 2023 20:56:39 +0200 Subject: [PATCH 04/22] PIN : fix some pin setup related code --- .../lockscreen/impl/components/PinEntryTextField.kt | 6 +++--- .../features/lockscreen/impl/pin/model/PinEntry.kt | 4 ++-- .../features/lockscreen/impl/setup/SetupPinNode.kt | 2 +- .../features/lockscreen/impl/setup/SetupPinView.kt | 9 +++++++++ 4 files changed, 15 insertions(+), 6 deletions(-) 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 index 91f6d435c5..a8e2b896f6 100644 --- 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 @@ -47,11 +47,11 @@ fun PinEntryTextField( ) { BasicTextField( modifier = modifier, - value = TextFieldValue(pinEntry.toText()), + value = pinEntry.toText(), onValueChange = { - onValueChange(it.text) + onValueChange(it) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), decorationBox = { PinEntryRow(pinEntry = pinEntry) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt index eaca592de9..96c3bec3ad 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntry.kt @@ -41,9 +41,9 @@ data class PinEntry( * @return the new PinEntry */ fun fillWith(text: String): PinEntry { - val newDigits = digits.toMutableList() + val newDigits = MutableList(size) { PinDigit.Empty } text.forEachIndexed { index, char -> - if (index < size) { + if (index < size && char.isDigit()) { newDigits[index] = PinDigit.Filled(char) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt index 7474289f1e..762b9b4bc8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt @@ -38,7 +38,7 @@ class SetupPinNode @AssistedInject constructor( val state = presenter.present() SetupPinView( state = state, - onBackClicked = { }, + onBackClicked = this::navigateUp, modifier = modifier ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index b8f40b06d0..b2174d2233 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -29,8 +29,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -105,12 +109,17 @@ private fun SetupPinContent( state: SetupPinState, modifier: Modifier = Modifier, ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } PinEntryTextField( state.activePinEntry, onValueChange = { state.eventSink(SetupPinEvents.OnPinEntryChanged(it)) }, modifier = modifier + .focusRequester(focusRequester) .padding(top = 36.dp) .fillMaxWidth() ) From 3733ee7e802361fa5be553ff67565cf21cd81b9b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Oct 2023 11:30:43 +0200 Subject: [PATCH 05/22] PinEntryTextField : add isSecured parameter --- .../impl/components/PinEntryTextField.kt | 16 ++++++++++++---- .../lockscreen/impl/setup/SetupPinView.kt | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) 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 index a8e2b896f6..017a926b61 100644 --- 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 @@ -30,7 +30,6 @@ 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 @@ -42,6 +41,7 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun PinEntryTextField( pinEntry: PinEntry, + isSecured: Boolean, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -53,7 +53,7 @@ fun PinEntryTextField( }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), decorationBox = { - PinEntryRow(pinEntry = pinEntry) + PinEntryRow(pinEntry = pinEntry, isSecured = isSecured) } ) } @@ -61,6 +61,7 @@ fun PinEntryTextField( @Composable private fun PinEntryRow( pinEntry: PinEntry, + isSecured: Boolean, modifier: Modifier = Modifier, ) { Row( @@ -69,7 +70,7 @@ private fun PinEntryRow( verticalAlignment = Alignment.CenterVertically, ) { for (digit in pinEntry.digits) { - PinDigitView(digit = digit) + PinDigitView(digit = digit, isSecured = isSecured) } } } @@ -77,6 +78,7 @@ private fun PinEntryRow( @Composable private fun PinDigitView( digit: PinDigit, + isSecured: Boolean, modifier: Modifier = Modifier, ) { val shape = RoundedCornerShape(8.dp) @@ -96,8 +98,13 @@ private fun PinDigitView( ) { if (digit is PinDigit.Filled) { + val text = if(isSecured) { + "•" + } else { + digit.value.toString() + } Text( - text = digit.toText(), + text = text, style = ElementTheme.typography.fontHeadingMdBold ) } @@ -111,6 +118,7 @@ internal fun PinEntryTextFieldPreview() { ElementPreview { PinEntryTextField( pinEntry = PinEntry.createEmpty(4).fillWith("12"), + isSecured = true, onValueChange = {}, ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt index b2174d2233..537a90d0b6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinView.kt @@ -114,7 +114,8 @@ private fun SetupPinContent( focusRequester.requestFocus() } PinEntryTextField( - state.activePinEntry, + pinEntry = state.activePinEntry, + isSecured = true, onValueChange = { state.eventSink(SetupPinEvents.OnPinEntryChanged(it)) }, From 080b77fc6955cc69fea021dc0cf3ecdc62a0c48c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Oct 2023 11:30:53 +0200 Subject: [PATCH 06/22] Setup pin : let time for ui to refresh before switching to confirmation step --- .../impl/setup/SetupPinPresenter.kt | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 3c380e6be7..6472b4f1c2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -17,6 +17,7 @@ package io.element.android.features.lockscreen.impl.setup import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,6 +28,7 @@ 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 kotlinx.coroutines.delay import javax.inject.Inject class SetupPinPresenter @Inject constructor( @@ -48,29 +50,38 @@ class SetupPinPresenter @Inject constructor( var setupPinFailure by remember { mutableStateOf(null) } + LaunchedEffect(choosePinEntry) { + if (choosePinEntry.isComplete()) { + when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { + is PinValidator.Result.Invalid -> { + setupPinFailure = pinValidationResult.failure + } + PinValidator.Result.Valid -> { + // Leave some time for the ui to refresh before showing confirmation + delay(150) + isConfirmationStep = true + } + } + } + } + + LaunchedEffect(confirmPinEntry) { + if (confirmPinEntry.isComplete()) { + if (confirmPinEntry == choosePinEntry) { + //TODO save in db and navigate to next screen + } else { + setupPinFailure = SetupPinFailure.PinsDontMatch + } + } + } fun handleEvents(event: SetupPinEvents) { when (event) { is SetupPinEvents.OnPinEntryChanged -> { if (isConfirmationStep) { confirmPinEntry = confirmPinEntry.fillWith(event.entryAsText) - if (confirmPinEntry.isComplete()) { - if (confirmPinEntry == choosePinEntry) { - //TODO save in db and navigate to next screen - } else { - setupPinFailure = SetupPinFailure.PinsDontMatch - } - } } else { choosePinEntry = choosePinEntry.fillWith(event.entryAsText) - if (choosePinEntry.isComplete()) { - when (val pinValidationResult = pinValidator.isPinValid(choosePinEntry)) { - is PinValidator.Result.Invalid -> { - setupPinFailure = pinValidationResult.failure - } - PinValidator.Result.Valid -> isConfirmationStep = true - } - } } } SetupPinEvents.ClearFailure -> { From b0f27c111e7159ab97f888c4f86f42c7ffc9b841 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Oct 2023 11:35:11 +0200 Subject: [PATCH 07/22] PIN : add some tests to PinEntry --- .../lockscreen/impl/pin/model/PinEntryTest.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt new file mode 100644 index 0000000000..38abb2d295 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/model/PinEntryTest.kt @@ -0,0 +1,81 @@ +/* + * 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.pin.model + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class PinEntryTest { + + @Test + fun `when using fillWith with empty string ensure pin is empty`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry.fillWith("") + assertThat(newPinEntry.isEmpty()).isTrue() + } + + @Test + fun `when using fillWith with bigger string than size ensure pin is complete`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry.fillWith("12345") + assertThat(newPinEntry.isComplete()).isTrue() + newPinEntry.assertText("1234") + } + + @Test + fun `when using fillWith with non digit string ensure pin is filtering`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry.fillWith("12aa") + newPinEntry.assertText("12") + } + + @Test + fun `when using clear ensure pin is empty`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry.clear() + assertThat(newPinEntry.isEmpty()).isTrue() + assertThat(newPinEntry.isComplete()).isFalse() + newPinEntry.assertText("") + } + + @Test + fun `when using deleteLast ensure pin correct`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry.fillWith("1234").deleteLast() + newPinEntry.assertText("123") + } + + @Test + fun `when using deleteLast with empty pin ensure pin is empty`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry.deleteLast() + assertThat(newPinEntry.isEmpty()).isTrue() + } + + @Test + fun `when using addDigit with complete pin ensure pin is complete`() { + val pinEntry = PinEntry.createEmpty(4) + val newPinEntry = pinEntry + .addDigit('1') + .addDigit('2') + .addDigit('3') + .addDigit('4') + .addDigit('5') + assertThat(newPinEntry.isComplete()).isTrue() + newPinEntry.assertText("1234") + } +} From 645c699a6b668dbd3204583d929df705b46854e2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Oct 2023 14:19:52 +0200 Subject: [PATCH 08/22] PIN : start branching logic --- .../android/appconfig/LockScreenConfig.kt | 5 + .../lockscreen/api/LockScreenStateService.kt | 1 - .../lockscreen/impl/LockScreenFlowNode.kt | 4 +- .../impl/pin/DefaultPinCodeManager.kt | 38 +++-- .../lockscreen/impl/pin/PinCodeManager.kt | 43 ++++-- .../storage/SharedPreferencesPinCodeStore.kt | 10 +- .../impl/settings/LockScreenSettingsEvents.kt | 1 - .../settings/LockScreenSettingsFlowNode.kt | 133 ++++++++++++++++++ .../impl/settings/LockScreenSettingsNode.kt | 13 +- .../settings/LockScreenSettingsPresenter.kt | 40 ++++-- .../impl/settings/LockScreenSettingsState.kt | 2 +- .../LockScreenSettingsStateProvider.kt | 2 +- .../impl/settings/LockScreenSettingsView.kt | 15 +- .../impl/setup/SetupPinPresenter.kt | 4 +- .../state/DefaultLockScreenStateService.kt | 28 ++-- .../impl/unlock/PinUnlockPresenter.kt | 34 +++-- .../lockscreen/impl/unlock/PinUnlockState.kt | 5 +- .../impl/unlock/PinUnlockStateProvider.kt | 3 +- .../lockscreen/impl/unlock/PinUnlockView.kt | 11 +- .../impl/KeyStoreSecretKeyProvider.kt | 2 +- 20 files changed, 316 insertions(+), 78 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 9427a1f9c7..5dd903b484 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -32,4 +32,9 @@ object LockScreenConfig { * The size of the PIN. */ const val PIN_SIZE = 4 + + /** + * Number of attempts before the user is logged out. + */ + const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt index 2f2e6b2376..299d7bbc15 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt @@ -23,5 +23,4 @@ interface LockScreenStateService { suspend fun entersForeground() suspend fun entersBackground() - suspend fun unlock() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt index 962a133424..b3de0f81ac 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -24,9 +24,11 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode @@ -74,7 +76,7 @@ class LockScreenFlowNode @AssistedInject constructor( createNode(buildContext) } NavTarget.Settings -> { - createNode(buildContext) + createNode(buildContext) } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index e7529e9280..d7674c76a6 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -22,17 +22,30 @@ import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.cryptography.api.SecretKeyProvider import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject -private const val SECRET_KEY_ALIAS = "SECRET_KEY_ALIAS_PIN_CODE" +private const val SECRET_KEY_ALIAS = "elementx.SECRET_KEY_ALIAS_PIN_CODE" @ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) class DefaultPinCodeManager @Inject constructor( private val secretKeyProvider: SecretKeyProvider, private val encryptionDecryptionService: EncryptionDecryptionService, private val pinCodeStore: PinCodeStore, ) : PinCodeManager { + private val callbacks = CopyOnWriteArrayList() + + override fun addCallback(callback: PinCodeManager.Callback) { + callbacks.add(callback) + } + + override fun removeCallback(callback: PinCodeManager.Callback) { + callbacks.remove(callback) + } + override suspend fun isPinCodeAvailable(): Boolean { return pinCodeStore.hasPinCode() } @@ -41,6 +54,7 @@ class DefaultPinCodeManager @Inject constructor( val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) + callbacks.forEach { it.onPinCodeCreated() } } override suspend fun verifyPinCode(pinCode: String): Boolean { @@ -48,7 +62,17 @@ class DefaultPinCodeManager @Inject constructor( return try { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) - decryptedPinCode.contentEquals(pinCode.toByteArray()) + val pinCodeToCheck = pinCode.toByteArray() + decryptedPinCode.contentEquals(pinCodeToCheck).also { isPinCodeCorrect -> + if (isPinCodeCorrect) { + pinCodeStore.resetCounter() + callbacks.forEach { callback -> + callback.onPinCodeVerified() + } + } else { + pinCodeStore.onWrongPin() + } + } } catch (failure: Throwable) { false } @@ -56,17 +80,11 @@ class DefaultPinCodeManager @Inject constructor( override suspend fun deletePinCode() { pinCodeStore.deleteEncryptedPinCode() + pinCodeStore.resetCounter() + callbacks.forEach { it.onPinCodeRemoved() } } override suspend fun getRemainingPinCodeAttemptsNumber(): Int { return pinCodeStore.getRemainingPinCodeAttemptsNumber() } - - override suspend fun onWrongPin(): Int { - return pinCodeStore.onWrongPin() - } - - override suspend fun resetCounter() { - pinCodeStore.resetCounter() - } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 5f84f5296d..2f0d44d9f2 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -21,6 +21,37 @@ package io.element.android.features.lockscreen.impl.pin * Implementation should take care of encrypting the pin code and storing it. */ interface PinCodeManager { + + /** + * Callbacks for pin code management events. + */ + interface Callback { + /** + * Called when the pin code is verified. + */ + fun onPinCodeVerified() = Unit + + /** + * Called when the pin code is created. + */ + fun onPinCodeCreated() = Unit + + /** + * Called when the pin code is removed. + */ + fun onPinCodeRemoved() = Unit + } + + /** + * Register a callback to be notified of pin code management events. + */ + fun addCallback(callback: Callback) + + /** + * Unregister callback to be notified of pin code management events. + */ + fun removeCallback(callback: Callback) + /** * @return true if a pin code is available. */ @@ -46,16 +77,4 @@ interface PinCodeManager { * @return the number of remaining attempts before the pin code is blocked. */ suspend fun getRemainingPinCodeAttemptsNumber(): Int - - /** - * Should be called when the pin code is incorrect. - * Will decrement the remaining attempts number. - * @return the number of remaining attempts before the pin code is blocked. - */ - suspend fun onWrongPin(): Int - - /** - * Resets the counter of attempts for PIN code. - */ - suspend fun resetCounter() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt index 5d53decadf..db84282ebc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt @@ -19,6 +19,7 @@ package io.element.android.features.lockscreen.impl.pin.storage import android.content.SharedPreferences import androidx.core.content.edit import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.LockScreenConfig import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.DefaultPreferences @@ -31,7 +32,6 @@ import javax.inject.Inject private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" -private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) @@ -57,8 +57,6 @@ class SharedPreferencesPinCodeStore @Inject constructor( } override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) { - // Also reset the counters - resetCounter() sharedPreferences.edit { remove(ENCODED_PIN_CODE_KEY) } @@ -72,14 +70,12 @@ class SharedPreferencesPinCodeStore @Inject constructor( } override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { - mutex.withLock { - sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) - } + sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) } override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { mutex.withLock { - val remaining = getRemainingPinCodeAttemptsNumber() - 1 + val remaining = (getRemainingPinCodeAttemptsNumber() - 1).coerceAtLeast(0) sharedPreferences.edit { putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt index 005077d81b..110ca542de 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt @@ -20,6 +20,5 @@ sealed interface LockScreenSettingsEvents { data object RemovePin : LockScreenSettingsEvents data object ConfirmRemovePin : LockScreenSettingsEvents data object CancelRemovePin : LockScreenSettingsEvents - data object ChangePin : LockScreenSettingsEvents data object ToggleBiometric : LockScreenSettingsEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt new file mode 100644 index 0000000000..2d84db1b7e --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.features.lockscreen.impl.setup.SetupPinNode +import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +class LockScreenSettingsFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val pinCodeManager: PinCodeManager, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Unknown, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + data object Unknown : NavTarget + + @Parcelize + data object Unlock : NavTarget + + @Parcelize + data object Setup : NavTarget + + @Parcelize + data object Settings : NavTarget + } + + private val pinCodeManagerCallback = object : PinCodeManager.Callback { + override fun onPinCodeVerified() { + backstack.newRoot(NavTarget.Settings) + } + + override fun onPinCodeCreated() { + backstack.newRoot(NavTarget.Settings) + } + + override fun onPinCodeRemoved() { + navigateUp() + } + } + + init { + lifecycleScope.launch { + if (pinCodeManager.isPinCodeAvailable()) { + backstack.newRoot(NavTarget.Unlock) + } else { + backstack.newRoot(NavTarget.Setup) + } + } + lifecycle.subscribe( + onCreate = { + pinCodeManager.addCallback(pinCodeManagerCallback) + }, + onDestroy = { + pinCodeManager.removeCallback(pinCodeManagerCallback) + } + ) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Unlock -> { + createNode(buildContext) + } + NavTarget.Setup -> { + createNode(buildContext) + } + NavTarget.Settings -> { + val callback = object : LockScreenSettingsNode.Callback { + override fun onChangePinClicked() { + backstack.push(NavTarget.Setup) + } + } + createNode(buildContext, plugins = listOf(callback)) + } + NavTarget.Unknown -> node(buildContext) { } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), + ) + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt index 6b47984076..3e19f24e68 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -33,12 +34,22 @@ class LockScreenSettingsNode @AssistedInject constructor( private val presenter: LockScreenSettingsPresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onChangePinClicked() + } + + private fun onChangePinClicked() { + plugins().forEach { it.onChangePinClicked() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() LockScreenSettingsView( state = state, - modifier = modifier + onBackPressed = this::navigateUp, + onChangePinClicked = this::onChangePinClicked, + modifier = modifier, ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index 4ac1d1c863..a91597448a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -17,41 +17,65 @@ package io.element.android.features.lockscreen.impl.settings import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject -class LockScreenSettingsPresenter @Inject constructor() : Presenter { +class LockScreenSettingsPresenter @Inject constructor( + private val pinCodeManager: PinCodeManager, + private val coroutineScope: CoroutineScope, +) : Presenter { @Composable override fun present(): LockScreenSettingsState { - + var triggerComputation by remember { + mutableIntStateOf(0) + } + var showRemovePinOption by remember { + mutableStateOf(false) + } var isBiometricEnabled by remember { mutableStateOf(false) } var showRemovePinConfirmation by remember { mutableStateOf(false) } + LaunchedEffect(triggerComputation) { + showRemovePinOption = !LockScreenConfig.IS_PIN_MANDATORY && pinCodeManager.isPinCodeAvailable() + } fun handleEvents(event: LockScreenSettingsEvents) { when (event) { - LockScreenSettingsEvents.CancelRemovePin -> TODO() - LockScreenSettingsEvents.ChangePin -> TODO() - LockScreenSettingsEvents.ConfirmRemovePin -> TODO() - LockScreenSettingsEvents.RemovePin -> TODO() - LockScreenSettingsEvents.ToggleBiometric -> TODO() + LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false + LockScreenSettingsEvents.ConfirmRemovePin -> { + coroutineScope.launch { + showRemovePinConfirmation = false + pinCodeManager.deletePinCode() + triggerComputation++ + } + } + LockScreenSettingsEvents.RemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvents.ToggleBiometric -> { + //TODO branch biometric logic + } } } return LockScreenSettingsState( - isPinMandatory = LockScreenConfig.IS_PIN_MANDATORY, + showRemovePinOption = showRemovePinOption, isBiometricEnabled = isBiometricEnabled, showRemovePinConfirmation = showRemovePinConfirmation, eventSink = ::handleEvents ) } + } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt index f076c76065..a6697d8a6d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsState.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.settings data class LockScreenSettingsState( - val isPinMandatory: Boolean, + val showRemovePinOption: Boolean, val isBiometricEnabled: Boolean, val showRemovePinConfirmation: Boolean, val eventSink: (LockScreenSettingsEvents) -> Unit diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt index 320ec1aa8b..b693b033b1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt @@ -32,7 +32,7 @@ fun aLockScreenSettingsState( isBiometricEnabled: Boolean = false, showRemovePinConfirmation: Boolean = false, ) = LockScreenSettingsState( - isPinMandatory = isLockMandatory, + showRemovePinOption = isLockMandatory, isBiometricEnabled = isBiometricEnabled, showRemovePinConfirmation = showRemovePinConfirmation, eventSink = {} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 55df5632f1..08234b6b07 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -34,21 +34,22 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun LockScreenSettingsView( state: LockScreenSettingsState, + onChangePinClicked: () -> Unit, + onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { PreferencePage( title = stringResource(id = io.element.android.libraries.ui.strings.R.string.common_screen_lock), + onBackPressed = onBackPressed, modifier = modifier ) { PreferenceCategory(showDivider = false) { PreferenceText( title = stringResource(id = R.string.screen_app_lock_settings_change_pin), - onClick = { - state.eventSink(LockScreenSettingsEvents.ChangePin) - } + onClick = onChangePinClicked ) PreferenceDivider() - if (!state.isPinMandatory) { + if (state.showRemovePinOption) { PreferenceText( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), tintColor = ElementTheme.colors.textCriticalPrimary, @@ -80,6 +81,10 @@ internal fun LockScreenSettingsViewPreview( @PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState, ) { ElementPreview { - LockScreenSettingsView(state) + LockScreenSettingsView( + state = state, + onChangePinClicked = {}, + onBackPressed = {}, + ) } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 6472b4f1c2..06de31281c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.validation.PinValidator import io.element.android.features.lockscreen.impl.setup.validation.SetupPinFailure @@ -34,6 +35,7 @@ import javax.inject.Inject class SetupPinPresenter @Inject constructor( private val pinValidator: PinValidator, private val buildMeta: BuildMeta, + private val pinCodeManager: PinCodeManager, ) : Presenter { @Composable @@ -68,7 +70,7 @@ class SetupPinPresenter @Inject constructor( LaunchedEffect(confirmPinEntry) { if (confirmPinEntry.isComplete()) { if (confirmPinEntry == choosePinEntry) { - //TODO save in db and navigate to next screen + pinCodeManager.createPinCode(confirmPinEntry.toText()) } else { setupPinFailure = SetupPinFailure.PinsDontMatch } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt index f2e037b111..601ae1b68a 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt @@ -19,12 +19,15 @@ package io.element.android.features.lockscreen.impl.state import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.lockscreen.api.LockScreenState import io.element.android.features.lockscreen.api.LockScreenStateService +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -36,6 +39,8 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultLockScreenStateService @Inject constructor( private val featureFlagService: FeatureFlagService, + private val pinCodeManager: PinCodeManager, + private val coroutineScope: CoroutineScope, ) : LockScreenStateService { private val _lockScreenState = MutableStateFlow(LockScreenState.Unlocked) @@ -43,10 +48,13 @@ class DefaultLockScreenStateService @Inject constructor( private var lockJob: Job? = null - override suspend fun unlock() { - if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - _lockScreenState.value = LockScreenState.Unlocked - } + init { + pinCodeManager.addCallback(object : PinCodeManager.Callback { + override fun onPinCodeVerified() { + _lockScreenState.value = LockScreenState.Unlocked + } + }) + coroutineScope.lockIfNeeded() } override suspend fun entersForeground() { @@ -54,11 +62,13 @@ class DefaultLockScreenStateService @Inject constructor( } override suspend fun entersBackground() = coroutineScope { - lockJob = launch { - if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - //delay(GRACE_PERIOD_IN_MILLIS) - _lockScreenState.value = LockScreenState.Locked - } + lockJob = lockIfNeeded() + } + + private fun CoroutineScope.lockIfNeeded(delayInMillis: Long = 0L) = launch { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && pinCodeManager.isPinCodeAvailable()) { + delay(delayInMillis) + _lockScreenState.value = LockScreenState.Locked } } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index e189a2ab39..c98ef75146 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -17,24 +17,22 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.appconfig.LockScreenConfig -import io.element.android.features.lockscreen.api.LockScreenStateService +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject class PinUnlockPresenter @Inject constructor( - private val pinStateService: LockScreenStateService, - private val coroutineScope: CoroutineScope, + private val pinCodeManager: PinCodeManager, ) : Presenter { @Composable @@ -43,9 +41,8 @@ class PinUnlockPresenter @Inject constructor( //TODO fetch size from db mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) } - var remainingAttempts by rememberSaveable { - //TODO fetch from db - mutableIntStateOf(3) + var remainingAttempts by remember { + mutableStateOf>(Async.Uninitialized) } var showWrongPinTitle by rememberSaveable { mutableStateOf(false) @@ -54,14 +51,25 @@ class PinUnlockPresenter @Inject constructor( mutableStateOf(false) } + LaunchedEffect(pinEntry) { + if (pinEntry.isComplete()) { + val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText()) + if (!isVerified) { + pinEntry = pinEntry.clear() + showWrongPinTitle = true + } + } + val remainingAttemptsNumber = pinCodeManager.getRemainingPinCodeAttemptsNumber() + remainingAttempts = Async.Success(remainingAttemptsNumber) + if (remainingAttemptsNumber == 0) { + showSignOutPrompt = true + } + } + fun handleEvents(event: PinUnlockEvents) { when (event) { is PinUnlockEvents.OnPinKeypadPressed -> { pinEntry = pinEntry.process(event.pinKeypadModel) - if (pinEntry.isComplete()) { - //TODO check pin with PinCodeManager - coroutineScope.launch { pinStateService.unlock() } - } } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 1787fb8e8b..c6f4dbb34d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -17,13 +17,14 @@ package io.element.android.features.lockscreen.impl.unlock import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.architecture.Async data class PinUnlockState( val pinEntry: PinEntry, val showWrongPinTitle: Boolean, - val remainingAttempts: Int, + val remainingAttempts: Async, val showSignOutPrompt: Boolean, val eventSink: (PinUnlockEvents) -> Unit ) { - val isSignOutPromptCancellable = remainingAttempts > 0 + val isSignOutPromptCancellable = (remainingAttempts.dataOrNull() ?: 0) > 0 } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 8ddc942e25..3358c47fe4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.lockscreen.impl.pin.model.PinEntry +import io.element.android.libraries.architecture.Async open class PinUnlockStateProvider : PreviewParameterProvider { override val values: Sequence @@ -38,7 +39,7 @@ fun aPinUnlockState( ) = PinUnlockState( pinEntry = pinEntry, showWrongPinTitle = showWrongPinTitle, - remainingAttempts = remainingAttempts, + remainingAttempts = Async.Success(remainingAttempts), showSignOutPrompt = showSignOutPrompt, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 5769f42b35..c87bb00ec8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -226,10 +226,15 @@ private fun PinUnlockHeader( color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.height(8.dp)) - val subtitle = if (state.showWrongPinTitle) { - pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = state.remainingAttempts, state.remainingAttempts) + val remainingAttempts = state.remainingAttempts.dataOrNull() + val subtitle = if (remainingAttempts != null) { + if (state.showWrongPinTitle) { + pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts) + } else { + stringResource(id = R.string.screen_app_lock_subtitle) + } } else { - stringResource(id = R.string.screen_app_lock_subtitle) + "" } val subtitleColor = if (state.showWrongPinTitle) { MaterialTheme.colorScheme.error diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt index 2cd09ea8f6..12be896a5b 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -40,7 +40,7 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider { // False positive lint issue @SuppressLint("WrongConstant") override fun getOrCreateKey(alias: String): SecretKey { - val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).also { it.load(null) } val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey return if (secretKeyEntry == null) { From 3e6077316602ecd946a5c50997910a1d62a8c8fd Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 24 Oct 2023 18:13:41 +0200 Subject: [PATCH 09/22] PIN : branch the mandatory flow --- .../android/appconfig/LockScreenConfig.kt | 7 +- .../android/appnav/LoggedInFlowNode.kt | 22 +--- features/ftue/impl/build.gradle.kts | 1 + .../features/ftue/impl/FtueFlowNode.kt | 15 ++- .../ftue/impl/state/DefaultFtueState.kt | 27 +++-- ...kScreenState.kt => LockScreenLockState.kt} | 6 +- ...enStateService.kt => LockScreenService.kt} | 14 ++- features/lockscreen/impl/build.gradle.kts | 4 + .../impl/DefaultLockScreenService.kt | 108 ++++++++++++++++++ .../lockscreen/impl/LockScreenFlowNode.kt | 2 - .../impl/pin/DefaultPinCodeManager.kt | 2 + .../state/DefaultLockScreenStateService.kt | 74 ------------ .../impl/unlock/PinUnlockPresenterTest.kt | 4 +- 13 files changed, 176 insertions(+), 110 deletions(-) rename features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/{LockScreenState.kt => LockScreenLockState.kt} (83%) rename features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/{LockScreenStateService.kt => LockScreenService.kt} (67%) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt delete mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 5dd903b484..c809e0c10e 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -21,7 +21,7 @@ object LockScreenConfig { /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = false + const val IS_PIN_MANDATORY: Boolean = true /** * Some PINs are blacklisted. @@ -37,4 +37,9 @@ object LockScreenConfig { * Number of attempts before the user is logged out. */ const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + + /** + * Time period before locking the app once backgrounded. + */ + const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 754ab167d1..8669738fc6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -49,8 +49,8 @@ import io.element.android.features.ftue.api.FtueEntryPoint import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint -import io.element.android.features.lockscreen.api.LockScreenState -import io.element.android.features.lockscreen.api.LockScreenStateService +import io.element.android.features.lockscreen.api.LockScreenLockState +import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint @@ -94,7 +94,7 @@ class LoggedInFlowNode @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val ftueState: FtueState, private val lockScreenEntryPoint: LockScreenEntryPoint, - private val lockScreenStateService: LockScreenStateService, + private val lockScreenStateService: LockScreenService, private val matrixClient: MatrixClient, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( @@ -134,16 +134,6 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.Ftue) } }, - onResume = { - coroutineScope.launch { - lockScreenStateService.entersForeground() - } - }, - onPause = { - coroutineScope.launch { - lockScreenStateService.entersBackground() - } - }, onStop = { coroutineScope.launch { //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. @@ -347,9 +337,9 @@ class LoggedInFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { - val lockScreenState by lockScreenStateService.state.collectAsState() + val lockScreenState by lockScreenStateService.lockState.collectAsState() when (lockScreenState) { - LockScreenState.Unlocked -> { + LockScreenLockState.Unlocked -> { Children( navModel = backstack, modifier = Modifier, @@ -361,7 +351,7 @@ class LoggedInFlowNode @AssistedInject constructor( PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) } } - LockScreenState.Locked -> { + LockScreenLockState.Locked -> { MoveActivityToBackgroundBackHandler() PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LockPermanent) } diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index 42fe8dade5..acdb97b517 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) implementation(projects.services.analytics.api) + implementation(projects.features.lockscreen.api) implementation(projects.libraries.permissions.api) implementation(projects.libraries.permissions.noop) implementation(projects.services.toolbox.api) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index ab6bf94a69..a8206ed927 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.features.ftue.impl.notifications.NotificationsOptInNod import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.welcome.WelcomeNode +import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -60,11 +61,12 @@ class FtueFlowNode @AssistedInject constructor( private val ftueState: DefaultFtueState, private val analyticsEntryPoint: AnalyticsEntryPoint, private val analyticsService: AnalyticsService, + private val lockScreenEntryPoint: LockScreenEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Placeholder, savedStateMap = buildContext.savedStateMap, - backPressHandler = NoOpBackstackHandlerStrategy(), + backPressHandler = NoOpBackstackHandlerStrategy(), ), buildContext = buildContext, plugins = plugins, @@ -85,6 +87,9 @@ class FtueFlowNode @AssistedInject constructor( @Parcelize data object AnalyticsOptIn : NavTarget + + @Parcelize + data object LockScreenSetup : NavTarget } private val callback = plugins.filterIsInstance().firstOrNull() @@ -139,6 +144,11 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.AnalyticsOptIn -> { analyticsEntryPoint.createNode(this, buildContext) } + NavTarget.LockScreenSetup -> { + lockScreenEntryPoint.nodeBuilder(this, buildContext) + .target(LockScreenEntryPoint.Target.Setup) + .build() + } } } @@ -156,6 +166,9 @@ class FtueFlowNode @AssistedInject constructor( FtueStep.AnalyticsOptIn -> { backstack.replace(NavTarget.AnalyticsOptIn) } + FtueStep.LockscreenSetup -> { + backstack.newRoot(NavTarget.LockScreenSetup) + } null -> callback?.onFtueFlowFinished() } } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt index 3247d7faf8..07d2372344 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt @@ -23,6 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.ftue.impl.migration.MigrationScreenStore import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState +import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.permissions.api.PermissionStateProvider @@ -44,6 +45,7 @@ class DefaultFtueState @Inject constructor( private val welcomeScreenState: WelcomeScreenState, private val migrationScreenStore: MigrationScreenStore, private val permissionStateProvider: PermissionStateProvider, + private val lockScreenService: LockScreenService, private val matrixClient: MatrixClient, ) : FtueState { @@ -72,10 +74,13 @@ class DefaultFtueState @Inject constructor( FtueStep.MigrationScreen -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep( FtueStep.WelcomeScreen ) - FtueStep.WelcomeScreen -> if (shouldAskNotificationPermissions()) FtueStep.NotificationsOptIn else getNextStep( + FtueStep.WelcomeScreen -> if (shouldAskNotificationPermissions()) FtueStep.NotificationsOptIn else getNextStep( FtueStep.NotificationsOptIn ) - FtueStep.NotificationsOptIn -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep( + FtueStep.NotificationsOptIn -> if (shouldDisplayLockscreenSetup()) FtueStep.LockscreenSetup else getNextStep( + FtueStep.LockscreenSetup + ) + FtueStep.LockscreenSetup -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep( FtueStep.AnalyticsOptIn ) FtueStep.AnalyticsOptIn -> null @@ -83,11 +88,12 @@ class DefaultFtueState @Inject constructor( private fun isAnyStepIncomplete(): Boolean { return listOf( - shouldDisplayMigrationScreen(), - shouldDisplayWelcomeScreen(), - shouldAskNotificationPermissions(), - needsAnalyticsOptIn() - ).any { it } + { shouldDisplayMigrationScreen() }, + { shouldDisplayWelcomeScreen() }, + { shouldAskNotificationPermissions() }, + { needsAnalyticsOptIn() }, + { shouldDisplayLockscreenSetup() }, + ).any { it -> it() } } private fun shouldDisplayMigrationScreen(): Boolean { @@ -112,6 +118,12 @@ class DefaultFtueState @Inject constructor( } else false } + private fun shouldDisplayLockscreenSetup(): Boolean { + return runBlocking { + lockScreenService.isSetupRequired() + } + } + fun setWelcomeScreenShown() { welcomeScreenState.setWelcomeScreenShown() updateState() @@ -128,4 +140,5 @@ sealed interface FtueStep { data object WelcomeScreen : FtueStep data object NotificationsOptIn : FtueStep data object AnalyticsOptIn : FtueStep + data object LockscreenSetup : FtueStep } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt similarity index 83% rename from features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt index d1e53cfdcc..e107729454 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenState.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenLockState.kt @@ -16,7 +16,7 @@ package io.element.android.features.lockscreen.api -sealed interface LockScreenState { - data object Unlocked : LockScreenState - data object Locked : LockScreenState +sealed interface LockScreenLockState { + data object Unlocked : LockScreenLockState + data object Locked : LockScreenLockState } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt similarity index 67% rename from features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt rename to features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt index 299d7bbc15..c6fe444119 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt @@ -18,9 +18,15 @@ package io.element.android.features.lockscreen.api import kotlinx.coroutines.flow.StateFlow -interface LockScreenStateService { - val state: StateFlow +interface LockScreenService { + /** + * The current lock state of the app. + */ + val lockState: StateFlow - suspend fun entersForeground() - suspend fun entersBackground() + /** + * Check if setting up the lock screen is required. + * @return true if the lock screen is mandatory and not setup yet, false otherwise. + */ + suspend fun isSetupRequired(): Boolean } diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index a3657ccff3..05e9ce20f3 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.sessionStorage.api) + implementation(projects.services.appnavstate.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -54,4 +56,6 @@ dependencies { testImplementation(projects.libraries.cryptography.test) testImplementation(projects.libraries.cryptography.impl) testImplementation(projects.libraries.featureflag.test) + implementation(projects.libraries.sessionStorage.test) + implementation(projects.services.appnavstate.test) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt new file mode 100644 index 0000000000..b949183892 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -0,0 +1,108 @@ +/* + * 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 + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.api.LockScreenLockState +import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.impl.pin.PinCodeManager +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.sessionstorage.api.observer.SessionListener +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultLockScreenService @Inject constructor( + private val featureFlagService: FeatureFlagService, + private val pinCodeManager: PinCodeManager, + private val coroutineScope: CoroutineScope, + private val sessionObserver: SessionObserver, + private val appForegroundStateService: AppForegroundStateService, +) : LockScreenService { + + private val _lockScreenState = MutableStateFlow(LockScreenLockState.Unlocked) + override val lockState: StateFlow = _lockScreenState + + private var lockJob: Job? = null + + init { + pinCodeManager.addCallback(object : PinCodeManager.Callback { + override fun onPinCodeVerified() { + _lockScreenState.value = LockScreenLockState.Unlocked + } + }) + coroutineScope.lockIfNeeded() + observeAppForegroundState() + observeSessionsState() + } + + /** + * Makes sure to delete the pin code when the session is deleted. + */ + private fun observeSessionsState() { + sessionObserver.addListener(object : SessionListener { + + override suspend fun onSessionCreated(userId: String) = Unit + + override suspend fun onSessionDeleted(userId: String) { + //TODO handle multi session at some point + pinCodeManager.deletePinCode() + } + }) + } + + /** + * Makes sure to lock the app if it goes in background for a certain amount of time. + */ + private fun observeAppForegroundState() { + coroutineScope.launch { + appForegroundStateService.start() + appForegroundStateService.isInForeground.collect { isInForeground -> + if (isInForeground) { + lockJob?.cancel() + } else { + lockJob = lockIfNeeded(delayInMillis = LockScreenConfig.GRACE_PERIOD_IN_MILLIS) + } + } + } + } + + override suspend fun isSetupRequired(): Boolean { + return LockScreenConfig.IS_PIN_MANDATORY + && featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) + && !pinCodeManager.isPinCodeAvailable() + } + + private fun CoroutineScope.lockIfNeeded(delayInMillis: Long = 0L) = launch { + if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && pinCodeManager.isPinCodeAvailable()) { + delay(delayInMillis) + _lockScreenState.value = LockScreenLockState.Locked + } + } +} 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 b3de0f81ac..a978b4d649 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenFlowNode.kt @@ -24,12 +24,10 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack -import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode -import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode import io.element.android.libraries.architecture.BackstackNode diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index d7674c76a6..b7b861199e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -23,6 +23,8 @@ import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.cryptography.api.SecretKeyProvider import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject 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 deleted file mode 100644 index 601ae1b68a..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ /dev/null @@ -1,74 +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.state - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.lockscreen.api.LockScreenState -import io.element.android.features.lockscreen.api.LockScreenStateService -import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -//private const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class DefaultLockScreenStateService @Inject constructor( - private val featureFlagService: FeatureFlagService, - private val pinCodeManager: PinCodeManager, - private val coroutineScope: CoroutineScope, -) : LockScreenStateService { - - private val _lockScreenState = MutableStateFlow(LockScreenState.Unlocked) - override val state: StateFlow = _lockScreenState - - private var lockJob: Job? = null - - init { - pinCodeManager.addCallback(object : PinCodeManager.Callback { - override fun onPinCodeVerified() { - _lockScreenState.value = LockScreenState.Unlocked - } - }) - coroutineScope.lockIfNeeded() - } - - override suspend fun entersForeground() { - lockJob?.cancel() - } - - override suspend fun entersBackground() = coroutineScope { - lockJob = lockIfNeeded() - } - - private fun CoroutineScope.lockIfNeeded(delayInMillis: Long = 0L) = launch { - if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && pinCodeManager.isPinCodeAvailable()) { - delay(delayInMillis) - _lockScreenState.value = LockScreenState.Locked - } - } -} diff --git a/features/lockscreen/impl/src/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 index 02919edce0..391a51e692 100644 --- 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 @@ -22,7 +22,7 @@ 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.DefaultLockScreenService 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 @@ -80,7 +80,7 @@ class PinUnlockPresenterTest { val featureFlagService = FakeFeatureFlagService().apply { setFeatureEnabled(FeatureFlags.PinUnlock, true) } - val lockScreenStateService = DefaultLockScreenStateService(featureFlagService) + val lockScreenStateService = DefaultLockScreenService(featureFlagService) return PinUnlockPresenter( lockScreenStateService, scope, From f105455bd6cffaf638985b2f96c6a65fb459b09f Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 11:15:15 +0200 Subject: [PATCH 10/22] PIN: branch SignOut --- .../impl/DefaultLockScreenService.kt | 4 ++ .../lockscreen/impl/LockScreenFlowNode.kt | 4 +- .../settings/LockScreenSettingsFlowNode.kt | 4 +- .../impl/settings/LockScreenSettingsNode.kt | 4 +- .../lockscreen/impl/setup/SetupPinNode.kt | 4 +- .../lockscreen/impl/unlock/PinUnlockEvents.kt | 2 + .../lockscreen/impl/unlock/PinUnlockNode.kt | 4 +- .../impl/unlock/PinUnlockPresenter.kt | 25 +++++++ .../lockscreen/impl/unlock/PinUnlockState.kt | 1 + .../impl/unlock/PinUnlockStateProvider.kt | 3 + .../lockscreen/impl/unlock/PinUnlockView.kt | 67 +++++++++++++------ .../impl/src/main/res/values/localazy.xml | 6 ++ .../api/src/main/res/values/localazy.xml | 14 ++-- .../impl/src/main/res/values/localazy.xml | 3 +- .../src/main/res/values/localazy.xml | 8 ++- tools/localazy/config.json | 3 +- 16 files changed, 120 insertions(+), 36 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index b949183892..afdc4e7899 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -56,6 +56,10 @@ class DefaultLockScreenService @Inject constructor( override fun onPinCodeVerified() { _lockScreenState.value = LockScreenLockState.Unlocked } + + override fun onPinCodeRemoved() { + _lockScreenState.value = LockScreenLockState.Unlocked + } }) coroutineScope.lockIfNeeded() observeAppForegroundState() 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 a978b4d649..31a11d06d7 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 @@ -34,10 +34,10 @@ import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope import kotlinx.parcelize.Parcelize -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class LockScreenFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index 2d84db1b7e..63d1937317 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -38,11 +38,11 @@ import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class LockScreenSettingsFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt index 3e19f24e68..96f5393483 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt @@ -25,9 +25,9 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class LockScreenSettingsNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt index 762b9b4bc8..a1342c816b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinNode.kt @@ -24,9 +24,9 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class SetupPinNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, 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 index 30ee16df02..18c45f5e4b 100644 --- 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 @@ -22,4 +22,6 @@ sealed interface PinUnlockEvents { data class OnPinKeypadPressed(val pinKeypadModel: PinKeypadModel) : PinUnlockEvents data object OnForgetPin : PinUnlockEvents data object ClearSignOutPrompt : PinUnlockEvents + data object SignOut : PinUnlockEvents + data object OnUseBiometric : PinUnlockEvents } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt index 0fba55c17b..88e076849e 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockNode.kt @@ -24,9 +24,9 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SessionScope -@ContributesNode(AppScope::class) +@ContributesNode(SessionScope::class) class PinUnlockNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index c98ef75146..e73650314b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -18,6 +18,7 @@ package io.element.android.features.lockscreen.impl.unlock import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,10 +30,16 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.MatrixClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject class PinUnlockPresenter @Inject constructor( private val pinCodeManager: PinCodeManager, + private val matrixClient: MatrixClient, + private val coroutineScope: CoroutineScope, ) : Presenter { @Composable @@ -51,6 +58,10 @@ class PinUnlockPresenter @Inject constructor( mutableStateOf(false) } + val signOutAction = remember { + mutableStateOf>(Async.Uninitialized) + } + LaunchedEffect(pinEntry) { if (pinEntry.isComplete()) { val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText()) @@ -73,6 +84,13 @@ class PinUnlockPresenter @Inject constructor( } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false + PinUnlockEvents.SignOut -> { + showSignOutPrompt = false + coroutineScope.signOut(signOutAction) + } + PinUnlockEvents.OnUseBiometric -> { + //TODO + } } } return PinUnlockState( @@ -80,10 +98,17 @@ class PinUnlockPresenter @Inject constructor( showWrongPinTitle = showWrongPinTitle, remainingAttempts = remainingAttempts, showSignOutPrompt = showSignOutPrompt, + signOutAction = signOutAction.value, eventSink = ::handleEvents ) } + private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { + suspend { + matrixClient.logout() + }.runCatchingUpdatingState(signOutAction) + } + private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { return when (pinKeypadModel) { PinKeypadModel.Back -> deleteLast() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index c6f4dbb34d..fa80254cce 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -24,6 +24,7 @@ data class PinUnlockState( val showWrongPinTitle: Boolean, val remainingAttempts: Async, val showSignOutPrompt: Boolean, + val signOutAction: Async, val eventSink: (PinUnlockEvents) -> Unit ) { val isSignOutPromptCancellable = (remainingAttempts.dataOrNull() ?: 0) > 0 diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index 3358c47fe4..cd2959dfe1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -28,6 +28,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), + aPinUnlockState(signOutAction = Async.Loading()), ) } @@ -36,10 +37,12 @@ fun aPinUnlockState( remainingAttempts: Int = 3, showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, + signOutAction: Async = Async.Uninitialized, ) = PinUnlockState( pinEntry = pinEntry, showWrongPinTitle = showWrongPinTitle, remainingAttempts = Async.Success(remainingAttempts), showSignOutPrompt = showSignOutPrompt, + signOutAction = signOutAction, eventSink = {} ) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index c87bb00ec8..00a59e63cc 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -48,6 +48,8 @@ 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.architecture.Async +import io.element.android.libraries.designsystem.components.ProgressDialog 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 @@ -79,7 +81,13 @@ fun PinUnlockView( } val footer = @Composable { PinUnlockFooter( - modifier = Modifier.padding(top = 24.dp) + modifier = Modifier.padding(top = 24.dp), + onUseBiometric = { + state.eventSink(PinUnlockEvents.OnUseBiometric) + }, + onForgotPin = { + state.eventSink(PinUnlockEvents.OnForgetPin) + }, ) } val content = @Composable { constraints: BoxWithConstraintsScope -> @@ -107,23 +115,42 @@ fun PinUnlockView( 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 = {}, - ) - } - } } + if (state.showSignOutPrompt) { + SignOutPrompt( + isCancellable = state.isSignOutPromptCancellable, + onSignOut = { state.eventSink(PinUnlockEvents.SignOut) }, + onDismiss = { state.eventSink(PinUnlockEvents.ClearSignOutPrompt) }, + ) + } + if (state.signOutAction is Async.Loading) { + ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content)) + } + } +} + +@Composable +private fun SignOutPrompt( + isCancellable: Boolean, + onSignOut: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + if (isCancellable) { + ConfirmationDialog( + title = stringResource(id = R.string.screen_app_lock_signout_alert_title), + content = stringResource(id = R.string.screen_app_lock_signout_alert_message), + onSubmitClicked = onSignOut, + onDismiss = onDismiss, + modifier = modifier, + ) + } 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 = onSignOut, + modifier = modifier, + ) } } @@ -255,11 +282,13 @@ private fun PinUnlockHeader( @Composable private fun PinUnlockFooter( + onUseBiometric: ()->Unit, + onForgotPin: ()->Unit, 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 = { }) + TextButton(text = "Use biometric", onClick = onUseBiometric) + TextButton(text = stringResource(id = R.string.screen_app_lock_forgot_pin), onClick = onForgotPin) } } diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index 6b12eac427..529785b74d 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -4,12 +4,17 @@ "Wrong PIN. You have %1$d more chance" "Wrong PIN. You have %1$d more chances" + "biometric authentication" + "biometric unlock" "Forgot PIN?" "Change PIN code" "Allow biometric unlock" "Remove PIN" "Are you sure you want to remove PIN?" "Remove PIN?" + "Allow %1$s" + "I’d rather use PIN" + "Save yourself some time and use %1$s to unlock the app each time" "Choose PIN" "Confirm PIN" "You cannot choose this as your PIN code for security reasons" @@ -22,4 +27,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th "You’ll need to re-login and create a new PIN to proceed" "You are being signed out" "You have 3 attempts to unlock" + "Signing out…" diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index c695309194..9296381c87 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -1,12 +1,18 @@ - "Please wait for this to complete before signing out." - "Your keys are still being backed up" "Are you sure you want to sign out?" "Sign out" "Signing out…" - "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." - "Recovery not set up" + "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages." + "You have turned off backup" + "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out." + "Your keys are still being backed up" + "Please wait for this to complete before signing out." + "Your keys are still being backed up" + "You are about to sign out of your last session. If you sign out now, you\'ll lose access to your encrypted messages." + "Recovery not set up" + "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages." + "Have you saved your recovery key?" "Sign out" "Sign out" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index bb285968c6..555ff574df 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -12,7 +12,8 @@ "Location" "Poll" "Text Formatting" - "Message history is currently unavailable in this room" + "Message history is currently unavailable." + "Message history is unavailable in this room. Verify this device to see your message history." "Could not retrieve user details" "Would you like to invite them back?" "You are alone in this chat" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index c70eceb21f..be27e21ff4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -51,7 +51,7 @@ "No" "Not now" "OK" - "Open settings" + "Settings" "Open with" "Quick reply" "Quote" @@ -145,6 +145,7 @@ "Server URL" "Settings" "Shared location" + "Signing out" "Starting chat…" "Sticker" "Success" @@ -167,8 +168,11 @@ "Video" "Voice message" "Waiting…" + "Waiting for decryption key" "Are you sure you want to end this poll?" "Poll: %1$s" + "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup." + "Confirm your recovery key" "Confirmation" "Warning" "Activities" @@ -268,6 +272,8 @@ If you proceed, some of your settings may change." "Enter…" "Recovery key confirmed" "Confirm your recovery key" + "Copied recovery key" + "Generating…" "Save recovery key" "Write down your recovery key somewhere safe or save it in a password manager." "Tap to copy recovery key" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 3371b80814..5144dc191d 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -158,7 +158,8 @@ { "name": ":features:lockscreen:impl", "includeRegex": [ - "screen_app_lock_.*" + "screen_app_lock_.*", + "screen_signout_in_progress_dialog_content" ] } ] From d59eaab7a8ea7fd94a3c1a933f40faf78955a7d7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 11:27:19 +0200 Subject: [PATCH 11/22] PIN: add callback on LockScreenEntryPoint --- .../features/ftue/impl/FtueFlowNode.kt | 6 +++++ .../lockscreen/api/LockScreenEntryPoint.kt | 6 +++++ .../impl/DefaultLockScreenEntryPoint.kt | 10 +++++++- .../lockscreen/impl/LockScreenFlowNode.kt | 25 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index a8206ed927..ab5d163e60 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -145,7 +145,13 @@ class FtueFlowNode @AssistedInject constructor( analyticsEntryPoint.createNode(this, buildContext) } NavTarget.LockScreenSetup -> { + val callback = object : LockScreenEntryPoint.Callback { + override fun onSetupCompleted() { + lifecycleScope.launch { moveToNextStep() } + } + } lockScreenEntryPoint.nodeBuilder(this, buildContext) + .callback(callback) .target(LockScreenEntryPoint.Target.Setup) .build() } diff --git a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt index fce3706dce..f63757717e 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.lockscreen.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint interface LockScreenEntryPoint : FeatureEntryPoint { @@ -25,10 +26,15 @@ interface LockScreenEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder fun target(target: Target): NodeBuilder fun build(): Node } + interface Callback: Plugin { + fun onSetupCompleted() + } + enum class Target { Settings, Setup, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 289f11113a..67182e4fff 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt @@ -30,8 +30,15 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LockScreenEntryPoint.NodeBuilder { var innerTarget: LockScreenEntryPoint.Target = LockScreenEntryPoint.Target.Unlock + val callbacks = mutableListOf() return object : LockScreenEntryPoint.NodeBuilder { + + override fun callback(callback: LockScreenEntryPoint.Callback): LockScreenEntryPoint.NodeBuilder { + callbacks += callback + return this + } + override fun target(target: LockScreenEntryPoint.Target): LockScreenEntryPoint.NodeBuilder { innerTarget = target return this @@ -45,7 +52,8 @@ class DefaultLockScreenEntryPoint @Inject constructor() : LockScreenEntryPoint { LockScreenEntryPoint.Target.Settings -> LockScreenFlowNode.NavTarget.Settings } ) - return parentNode.createNode(buildContext, listOf(inputs)) + val plugins = listOf(inputs) + callbacks + return parentNode.createNode(buildContext, plugins) } } } 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 31a11d06d7..e10ca406fb 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 @@ -20,13 +20,17 @@ import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins 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.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode @@ -41,6 +45,7 @@ import kotlinx.parcelize.Parcelize class LockScreenFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val pinCodeManager: PinCodeManager, ) : BackstackNode( backstack = BackStack( initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialNavTarget, @@ -65,6 +70,26 @@ class LockScreenFlowNode @AssistedInject constructor( data object Settings : NavTarget } + private val pinCodeManagerCallback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + plugins().forEach { + it.onSetupCompleted() + } + } + } + + override fun onBuilt() { + super.onBuilt() + lifecycle.subscribe( + onCreate = { + pinCodeManager.addCallback(pinCodeManagerCallback) + }, + onDestroy = { + pinCodeManager.removeCallback(pinCodeManagerCallback) + } + ) + } + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Unlock -> { From 432e20961821b2433c54573bb22e413f35377f51 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 15:07:05 +0200 Subject: [PATCH 12/22] PIN unlock : makes sure to load the pin size from storage --- .../android/appconfig/LockScreenConfig.kt | 2 +- .../impl/components/PinEntryTextField.kt | 10 ++-- .../impl/pin/DefaultPinCodeManager.kt | 9 ++- .../lockscreen/impl/pin/PinCodeManager.kt | 5 ++ .../impl/unlock/PinUnlockPresenter.kt | 56 ++++++++++++++----- .../lockscreen/impl/unlock/PinUnlockState.kt | 2 +- .../impl/unlock/PinUnlockStateProvider.kt | 2 +- .../lockscreen/impl/unlock/PinUnlockView.kt | 10 ++-- .../impl/src/main/res/values/localazy.xml | 5 +- 9 files changed, 73 insertions(+), 28 deletions(-) diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index c809e0c10e..75cf8406ed 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -21,7 +21,7 @@ object LockScreenConfig { /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = true + const val IS_PIN_MANDATORY: Boolean = false /** * Some PINs are blacklisted. 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 index 017a926b61..e4869e71d8 100644 --- 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 @@ -20,7 +20,8 @@ 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.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -58,16 +59,17 @@ fun PinEntryTextField( ) } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun PinEntryRow( pinEntry: PinEntry, isSecured: Boolean, modifier: Modifier = Modifier, ) { - Row( + FlowRow( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { for (digit in pinEntry.digits) { PinDigitView(digit = digit, isSecured = isSecured) @@ -98,7 +100,7 @@ private fun PinDigitView( ) { if (digit is PinDigit.Filled) { - val text = if(isSecured) { + val text = if (isSecured) { "•" } else { digit.value.toString() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt index b7b861199e..e5e2dc6106 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManager.kt @@ -23,8 +23,6 @@ import io.element.android.libraries.cryptography.api.EncryptionResult import io.element.android.libraries.cryptography.api.SecretKeyProvider import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.sessionstorage.api.observer.SessionObserver -import kotlinx.coroutines.launch import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -52,6 +50,13 @@ class DefaultPinCodeManager @Inject constructor( return pinCodeStore.hasPinCode() } + override suspend fun getPinCodeSize(): Int { + val encryptedPinCode = pinCodeStore.getEncryptedCode() ?: return 0 + val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) + val decryptedPinCode = encryptionDecryptionService.decrypt(secretKey, EncryptionResult.fromBase64(encryptedPinCode)) + return decryptedPinCode.size + } + override suspend fun createPinCode(pinCode: String) { val secretKey = secretKeyProvider.getOrCreateKey(SECRET_KEY_ALIAS) val encryptedPinCode = encryptionDecryptionService.encrypt(secretKey, pinCode.toByteArray()).toBase64() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index 2f0d44d9f2..c214e533ab 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -57,6 +57,11 @@ interface PinCodeManager { */ suspend fun isPinCodeAvailable(): Boolean + /** + * @return the size of the saved pin code. + */ + suspend fun getPinCodeSize(): Int + /** * Creates a new encrypted pin code. * @param pinCode the clear pin code to create diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index e73650314b..19f075b5b5 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -24,13 +24,13 @@ 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.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.matrix.api.MatrixClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -44,10 +44,10 @@ class PinUnlockPresenter @Inject constructor( @Composable override fun present(): PinUnlockState { - var pinEntry by remember { - //TODO fetch size from db - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + val pinEntryState = remember { + mutableStateOf>(Async.Uninitialized) } + val pinEntry by pinEntryState var remainingAttempts by remember { mutableStateOf>(Async.Uninitialized) } @@ -62,11 +62,18 @@ class PinUnlockPresenter @Inject constructor( mutableStateOf>(Async.Uninitialized) } + LaunchedEffect(Unit) { + suspend { + val pinCodeSize = pinCodeManager.getPinCodeSize() + PinEntry.createEmpty(pinCodeSize) + }.runCatchingUpdatingState(pinEntryState) + } + LaunchedEffect(pinEntry) { if (pinEntry.isComplete()) { val isVerified = pinCodeManager.verifyPinCode(pinEntry.toText()) if (!isVerified) { - pinEntry = pinEntry.clear() + pinEntryState.value = pinEntry.clear() showWrongPinTitle = true } } @@ -80,7 +87,7 @@ class PinUnlockPresenter @Inject constructor( fun handleEvents(event: PinUnlockEvents) { when (event) { is PinUnlockEvents.OnPinKeypadPressed -> { - pinEntry = pinEntry.process(event.pinKeypadModel) + pinEntryState.value = pinEntry.process(event.pinKeypadModel) } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false @@ -103,17 +110,38 @@ class PinUnlockPresenter @Inject constructor( ) } + private fun Async.isComplete(): Boolean { + return dataOrNull()?.isComplete().orFalse() + } + + private fun Async.toText(): String { + return dataOrNull()?.toText() ?: "" + } + + private fun Async.clear(): Async { + return when (this) { + is Async.Success -> Async.Success(data.clear()) + else -> this + } + } + + private fun Async.process(pinKeypadModel: PinKeypadModel): Async { + return when (this) { + is Async.Success -> { + val pinEntry = when (pinKeypadModel) { + PinKeypadModel.Back -> data.deleteLast() + is PinKeypadModel.Number -> data.addDigit(pinKeypadModel.number) + PinKeypadModel.Empty -> data + } + Async.Success(pinEntry) + } + else -> this + } + } + private fun CoroutineScope.signOut(signOutAction: MutableState>) = launch { suspend { matrixClient.logout() }.runCatchingUpdatingState(signOutAction) } - - 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 index fa80254cce..178e8692d1 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -20,7 +20,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.libraries.architecture.Async data class PinUnlockState( - val pinEntry: PinEntry, + val pinEntry: Async, val showWrongPinTitle: Boolean, val remainingAttempts: Async, val showSignOutPrompt: Boolean, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt index cd2959dfe1..009a5bf4a4 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt @@ -39,7 +39,7 @@ fun aPinUnlockState( showSignOutPrompt: Boolean = false, signOutAction: Async = Async.Uninitialized, ) = PinUnlockState( - pinEntry = pinEntry, + pinEntry = Async.Success(pinEntry), showWrongPinTitle = showWrongPinTitle, remainingAttempts = Async.Success(remainingAttempts), showSignOutPrompt = showSignOutPrompt, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 00a59e63cc..514531a718 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -258,7 +258,7 @@ private fun PinUnlockHeader( if (state.showWrongPinTitle) { pluralStringResource(id = R.plurals.screen_app_lock_subtitle_wrong_pin, count = remainingAttempts, remainingAttempts) } else { - stringResource(id = R.string.screen_app_lock_subtitle) + pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts) } } else { "" @@ -276,14 +276,16 @@ private fun PinUnlockHeader( color = subtitleColor, ) Spacer(Modifier.height(24.dp)) - PinDotsRow(state.pinEntry) + if (state.pinEntry is Async.Success) { + PinDotsRow(state.pinEntry.data) + } } } @Composable private fun PinUnlockFooter( - onUseBiometric: ()->Unit, - onForgotPin: ()->Unit, + onUseBiometric: () -> Unit, + onForgotPin: () -> Unit, modifier: Modifier = Modifier, ) { Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) { diff --git a/features/lockscreen/impl/src/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index 529785b74d..bd6d522d57 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + + "You have %1$d attempt to unlock" + "You have %1$d attempts to unlock" + "Wrong PIN. You have %1$d more chance" "Wrong PIN. You have %1$d more chances" @@ -26,6 +30,5 @@ Choose something memorable. If you forget this PIN, you will be logged out of th "PINs don\'t match" "You’ll need to re-login and create a new PIN to proceed" "You are being signed out" - "You have 3 attempts to unlock" "Signing out…" From ed4815c40abb1d2db54cfbc5d002778d4b37f244 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:13:30 +0200 Subject: [PATCH 13/22] PIN: fix and add tests --- .../impl/unlock/PinUnlockPresenter.kt | 7 +- .../lockscreen/impl/pin/PinCodeManager.kt | 28 +++++ .../impl/setup/SetupPinPresenterTest.kt | 19 +++- .../impl/unlock/PinUnlockPresenterTest.kt | 105 +++++++++++++++--- 4 files changed, 135 insertions(+), 24 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt index 19f075b5b5..b26f967b4c 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenter.kt @@ -57,7 +57,6 @@ class PinUnlockPresenter @Inject constructor( var showSignOutPrompt by rememberSaveable { mutableStateOf(false) } - val signOutAction = remember { mutableStateOf>(Async.Uninitialized) } @@ -92,8 +91,10 @@ class PinUnlockPresenter @Inject constructor( PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false PinUnlockEvents.SignOut -> { - showSignOutPrompt = false - coroutineScope.signOut(signOutAction) + if (showSignOutPrompt) { + showSignOutPrompt = false + coroutineScope.signOut(signOutAction) + } } PinUnlockEvents.OnUseBiometric -> { //TODO diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt new file mode 100644 index 0000000000..a2e2dacf97 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -0,0 +1,28 @@ +/* + * 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.pin + +import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService +import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider + +internal fun createPinCodeManager(): PinCodeManager { + val pinCodeStore = InMemoryPinCodeStore() + val secretKeyProvider = SimpleSecretKeyProvider() + val encryptionDecryptionService = AESEncryptionDecryptionService() + return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index ff797b52f4..0cc38dd355 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,12 +20,15 @@ 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.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.createPinCodeManager 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.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Test @@ -38,8 +41,13 @@ class SetupPinPresenterTest { @Test fun `present - complete flow`() = runTest { - - val presenter = createSetupPinPresenter() + val pinCodeCreated = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeCreated.complete(Unit) + } + } + val presenter = createSetupPinPresenter(callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -96,10 +104,13 @@ class SetupPinPresenterTest { state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertText(completePin) } + pinCodeCreated.await() } } - private fun createSetupPinPresenter(): SetupPinPresenter { - return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta()) + private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter { + val pinCodeManager = createPinCodeManager() + pinCodeManager.addCallback(callback) + return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta(), pinCodeManager) } } 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 index 391a51e692..16e44b2af0 100644 --- 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 @@ -20,13 +20,16 @@ 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.PinCodeManager +import io.element.android.features.lockscreen.impl.pin.createPinCodeManager +import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText -import io.element.android.features.lockscreen.impl.DefaultLockScreenService 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.libraries.architecture.Async +import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,16 +40,27 @@ class PinUnlockPresenterTest { private val completePin = "1235" @Test - fun `present - complete flow`() = runTest { - val presenter = createPinUnlockPresenter(this) + fun `present - success verify flow`() = runTest { + val pinCodeVerified = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeVerified.complete(Unit) + } + } + val presenter = createPinUnlockPresenter(this, callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { awaitItem().also { state -> - state.pinEntry.assertEmpty() + assertThat(state.pinEntry).isInstanceOf(Async.Uninitialized::class.java) assertThat(state.showWrongPinTitle).isFalse() assertThat(state.showSignOutPrompt).isFalse() - assertThat(state.remainingAttempts).isEqualTo(3) + assertThat(state.signOutAction).isInstanceOf(Async.Uninitialized::class.java) + assertThat(state.remainingAttempts).isInstanceOf(Async.Uninitialized::class.java) + } + consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last().also { state -> state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) } @@ -55,9 +69,55 @@ class PinUnlockPresenterTest { state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Back)) state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Empty)) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) } awaitLastSequentialItem().also { state -> - state.pinEntry.assertText(halfCompletePin) + state.pinEntry.assertText(completePin) + } + pinCodeVerified.await() + } + } + + @Test + fun `present - failure verify flow`() = runTest { + val pinCodeVerified = CompletableDeferred() + val callback = object : PinCodeManager.Callback { + override fun onPinCodeCreated() { + pinCodeVerified.complete(Unit) + } + } + val presenter = createPinUnlockPresenter(this, callback) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last() + val numberOfAttempts = initialState.remainingAttempts.dataOrNull() ?: 0 + repeat(numberOfAttempts) { + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('1'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('2'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) + initialState.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('4'))) + } + awaitLastSequentialItem().also { state -> + assertThat(state.remainingAttempts.dataOrNull()).isEqualTo(0) + assertThat(state.showSignOutPrompt).isEqualTo(true) + assertThat(state.isSignOutPromptCancellable).isEqualTo(false) + } + } + } + + @Test + fun `present - forgot pin flow`() = runTest { + val presenter = createPinUnlockPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { + it.pinEntry is Async.Success && it.remainingAttempts is Async.Success + }.last().also { state -> state.eventSink(PinUnlockEvents.OnForgetPin) } awaitLastSequentialItem().also { state -> @@ -67,22 +127,33 @@ class PinUnlockPresenterTest { } awaitLastSequentialItem().also { state -> assertThat(state.showSignOutPrompt).isEqualTo(false) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('3'))) - state.eventSink(PinUnlockEvents.OnPinKeypadPressed(PinKeypadModel.Number('5'))) + state.eventSink(PinUnlockEvents.OnForgetPin) } awaitLastSequentialItem().also { state -> - state.pinEntry.assertText(completePin) + assertThat(state.showSignOutPrompt).isEqualTo(true) + state.eventSink(PinUnlockEvents.SignOut) + } + consumeItemsUntilPredicate { state -> + state.signOutAction is Async.Success } } } - private suspend fun createPinUnlockPresenter(scope: CoroutineScope): PinUnlockPresenter { - val featureFlagService = FakeFeatureFlagService().apply { - setFeatureEnabled(FeatureFlags.PinUnlock, true) + private fun Async.assertText(text: String) { + dataOrNull()?.assertText(text) + } + + private suspend fun createPinUnlockPresenter( + scope: CoroutineScope, + callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, + ): PinUnlockPresenter { + val pinCodeManager = createPinCodeManager().apply { + addCallback(callback) + createPinCode(completePin) } - val lockScreenStateService = DefaultLockScreenService(featureFlagService) return PinUnlockPresenter( - lockScreenStateService, + pinCodeManager, + FakeMatrixClient(), scope, ) } From 1d314e198a0eb9c25c898aa9102dca4605d135d8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:45:42 +0200 Subject: [PATCH 14/22] PIN: clean pin code storage --- features/lockscreen/impl/build.gradle.kts | 1 + .../impl/pin/storage/PinCodeStore.kt | 17 +-- .../pin/storage/PreferencesPinCodeStore.kt | 89 +++++++++++++++ .../storage/SharedPreferencesPinCodeStore.kt | 101 ------------------ .../impl/pin/storage/InMemoryPinCodeStore.kt | 12 +-- 5 files changed, 93 insertions(+), 127 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt delete mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 05e9ce20f3..fdb9dcd178 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) + implementation(libs.androidx.datastore.preferences) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt index e72cbca2db..818bc6a47d 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PinCodeStore.kt @@ -18,10 +18,6 @@ package io.element.android.features.lockscreen.impl.pin.storage interface PinCodeStore : EncryptedPinCodeStorage { - interface Listener { - fun onPinSetUpChange(isConfigured: Boolean) - } - /** * Returns the remaining PIN code attempts. When this reaches 0 the PIN code access won't be available for some time. */ @@ -29,24 +25,13 @@ interface PinCodeStore : EncryptedPinCodeStorage { /** * Should decrement the number of remaining PIN code attempts. - * @return The remaining attempts. */ - suspend fun onWrongPin(): Int + suspend fun onWrongPin() /** * Resets the counter of attempts for PIN code and biometric access. */ suspend fun resetCounter() - - /** - * Adds a listener to be notified when the PIN code us created or removed. - */ - fun addListener(listener: Listener) - - /** - * Removes a listener to be notified when the PIN code us created or removed. - */ - fun removeListener(listener: Listener) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt new file mode 100644 index 0000000000..2bf9225a2c --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.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.pin.storage + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.LockScreenConfig +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +private val Context.dataStore: DataStore by preferencesDataStore(name = "pin_code_store") + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class PreferencesPinCodeStore @Inject constructor( + @ApplicationContext private val context: Context, +) : PinCodeStore { + + private val pinCodeKey = stringPreferencesKey("encoded_pin_code") + private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts") + + override suspend fun getRemainingPinCodeAttemptsNumber(): Int { + return context.dataStore.data.map { preferences -> + preferences[remainingAttemptsKey] ?: 0 + }.first() + } + + override suspend fun onWrongPin() { + context.dataStore.edit { preferences -> + val current = preferences[remainingAttemptsKey] ?: 0 + val remaining = (current - 1).coerceAtLeast(0) + preferences[remainingAttemptsKey] = remaining + } + } + + override suspend fun resetCounter() { + context.dataStore.edit { preferences -> + preferences[remainingAttemptsKey] = LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + } + } + + override suspend fun getEncryptedCode(): String? { + return context.dataStore.data.map { preferences -> + preferences[pinCodeKey] + }.first() + } + + override suspend fun saveEncryptedPinCode(pinCode: String) { + context.dataStore.edit { preferences -> + preferences[pinCodeKey] = pinCode + } + } + + override suspend fun deleteEncryptedPinCode() { + context.dataStore.edit { preferences -> + preferences.remove(pinCodeKey) + } + } + + override suspend fun hasPinCode(): Boolean { + return context.dataStore.data.map { preferences -> + preferences[pinCodeKey] != null + }.first() + } +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt deleted file mode 100644 index db84282ebc..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ /dev/null @@ -1,101 +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.pin.storage - -import android.content.SharedPreferences -import androidx.core.content.edit -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.LockScreenConfig -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.DefaultPreferences -import io.element.android.libraries.di.SingleIn -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject - -private const val ENCODED_PIN_CODE_KEY = "ENCODED_PIN_CODE_KEY" -private const val REMAINING_PIN_CODE_ATTEMPTS_KEY = "REMAINING_PIN_CODE_ATTEMPTS_KEY" - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class SharedPreferencesPinCodeStore @Inject constructor( - private val dispatchers: CoroutineDispatchers, - @DefaultPreferences private val sharedPreferences: SharedPreferences, -) : PinCodeStore { - - private val listeners = CopyOnWriteArrayList() - private val mutex = Mutex() - - override suspend fun getEncryptedCode(): String? = withContext(dispatchers.io) { - sharedPreferences.getString(ENCODED_PIN_CODE_KEY, null) - } - - override suspend fun saveEncryptedPinCode(pinCode: String) = withContext(dispatchers.io) { - sharedPreferences.edit { - putString(ENCODED_PIN_CODE_KEY, pinCode) - } - withContext(dispatchers.main) { - listeners.forEach { it.onPinSetUpChange(isConfigured = true) } - } - } - - override suspend fun deleteEncryptedPinCode() = withContext(dispatchers.io) { - sharedPreferences.edit { - remove(ENCODED_PIN_CODE_KEY) - } - withContext(dispatchers.main) { - listeners.forEach { it.onPinSetUpChange(isConfigured = false) } - } - } - - override suspend fun hasPinCode(): Boolean = withContext(dispatchers.io) { - sharedPreferences.contains(ENCODED_PIN_CODE_KEY) - } - - override suspend fun getRemainingPinCodeAttemptsNumber(): Int = withContext(dispatchers.io) { - sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) - } - - override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { - mutex.withLock { - val remaining = (getRemainingPinCodeAttemptsNumber() - 1).coerceAtLeast(0) - sharedPreferences.edit { - putInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, remaining) - } - remaining - } - } - - override suspend fun resetCounter() = withContext(dispatchers.io) { - mutex.withLock { - sharedPreferences.edit { - remove(REMAINING_PIN_CODE_ATTEMPTS_KEY) - } - } - } - - override fun addListener(listener: PinCodeStore.Listener) { - listeners.add(listener) - } - - override fun removeListener(listener: PinCodeStore.Listener) { - listeners.remove(listener) - } -} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt index 0b7c2f256b..6b6597728c 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/storage/InMemoryPinCodeStore.kt @@ -27,22 +27,14 @@ class InMemoryPinCodeStore : PinCodeStore { return remainingAttempts } - override suspend fun onWrongPin(): Int { - return remainingAttempts-- + override suspend fun onWrongPin() { + remainingAttempts-- } override suspend fun resetCounter() { remainingAttempts = DEFAULT_REMAINING_ATTEMPTS } - override fun addListener(listener: PinCodeStore.Listener) { - // no-op - } - - override fun removeListener(listener: PinCodeStore.Listener) { - // no-op - } - override suspend fun getEncryptedCode(): String? { return pinCode } From b13567019602f3f0da76b7da10b35d66a07cab8a Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 16:54:34 +0200 Subject: [PATCH 15/22] Pin : clean remaining pin code attempts --- .../lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt index 2bf9225a2c..04f849e117 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -45,13 +45,13 @@ class PreferencesPinCodeStore @Inject constructor( override suspend fun getRemainingPinCodeAttemptsNumber(): Int { return context.dataStore.data.map { preferences -> - preferences[remainingAttemptsKey] ?: 0 + preferences.getRemainingPinCodeAttemptsNumber() }.first() } override suspend fun onWrongPin() { context.dataStore.edit { preferences -> - val current = preferences[remainingAttemptsKey] ?: 0 + val current = preferences.getRemainingPinCodeAttemptsNumber() val remaining = (current - 1).coerceAtLeast(0) preferences[remainingAttemptsKey] = remaining } @@ -86,4 +86,6 @@ class PreferencesPinCodeStore @Inject constructor( preferences[pinCodeKey] != null }.first() } + + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT } From a34604512d2a9cdc590028fa8e092ed1711fdf52 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 25 Oct 2023 18:37:02 +0200 Subject: [PATCH 16/22] Pin: add tests and make LockScreenConfig an injectable data class --- app/build.gradle.kts | 1 + appconfig/build.gradle.kts | 11 +++ .../android/appconfig/LockScreenConfig.kt | 34 ++++++-- .../impl/DefaultLockScreenService.kt | 5 +- .../pin/storage/PreferencesPinCodeStore.kt | 5 +- .../impl/settings/LockScreenSettingsEvents.kt | 2 +- .../settings/LockScreenSettingsPresenter.kt | 14 ++-- .../impl/settings/LockScreenSettingsView.kt | 2 +- .../impl/setup/SetupPinPresenter.kt | 5 +- .../impl/setup/validation/PinValidator.kt | 7 +- .../impl/fixtures/LockScreenConfig.kt | 35 ++++++++ .../impl/{pin => fixtures}/PinCodeManager.kt | 15 ++-- .../LockScreenSettingsPresenterTest.kt | 79 +++++++++++++++++++ .../impl/setup/SetupPinPresenterTest.kt | 20 ++++- .../impl/unlock/PinUnlockPresenterTest.kt | 4 +- 15 files changed, 203 insertions(+), 36 deletions(-) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt rename features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/{pin => fixtures}/PinCodeManager.kt (59%) create mode 100644 features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ac84cfec2..324da8447d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -201,6 +201,7 @@ dependencies { implementation(projects.features.call) implementation(projects.anvilannotations) implementation(projects.appnav) + implementation(projects.appconfig) anvil(projects.anvilcodegen) implementation(libs.appyx.core) diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 3c03739553..3f9275d383 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -16,9 +16,20 @@ plugins { id("java-library") alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.di) +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 75cf8406ed..e73b3cd150 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -16,30 +16,52 @@ package io.element.android.appconfig -object LockScreenConfig { +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +/** + * Configuration for the lock screen feature. + */ +data class LockScreenConfig( /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = false + val isPinMandatory: Boolean, /** * Some PINs are blacklisted. */ - val PIN_BLACKLIST = setOf("0000", "1234") + val pinBlacklist: Set, /** * The size of the PIN. */ - const val PIN_SIZE = 4 + val pinSize: Int, /** * Number of attempts before the user is logged out. */ - const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + val maxPinCodeAttemptsBeforeLogout: Int, /** * Time period before locking the app once backgrounded. */ - const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L + val gracePeriodInMillis: Long +) + +@ContributesTo(AppScope::class) +@Module +object LockScreenConfigModule { + + @Provides + fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( + isPinMandatory = false, + pinBlacklist = setOf("0000", "1234"), + pinSize = 4, + maxPinCodeAttemptsBeforeLogout = 3, + gracePeriodInMillis = 90_000L + ) + } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index afdc4e7899..32e1e3d1b9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -39,6 +39,7 @@ import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultLockScreenService @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val featureFlagService: FeatureFlagService, private val pinCodeManager: PinCodeManager, private val coroutineScope: CoroutineScope, @@ -91,14 +92,14 @@ class DefaultLockScreenService @Inject constructor( if (isInForeground) { lockJob?.cancel() } else { - lockJob = lockIfNeeded(delayInMillis = LockScreenConfig.GRACE_PERIOD_IN_MILLIS) + lockJob = lockIfNeeded(delayInMillis = lockScreenConfig.gracePeriodInMillis) } } } } override suspend fun isSetupRequired(): Boolean { - return LockScreenConfig.IS_PIN_MANDATORY + return lockScreenConfig.isPinMandatory && featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && !pinCodeManager.isPinCodeAvailable() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt index 04f849e117..8631c05502 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -38,6 +38,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class PreferencesPinCodeStore @Inject constructor( @ApplicationContext private val context: Context, + private val lockScreenConfig: LockScreenConfig, ) : PinCodeStore { private val pinCodeKey = stringPreferencesKey("encoded_pin_code") @@ -59,7 +60,7 @@ class PreferencesPinCodeStore @Inject constructor( override suspend fun resetCounter() { context.dataStore.edit { preferences -> - preferences[remainingAttemptsKey] = LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout } } @@ -87,5 +88,5 @@ class PreferencesPinCodeStore @Inject constructor( }.first() } - private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt index 110ca542de..9032e8d0ef 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.settings sealed interface LockScreenSettingsEvents { - data object RemovePin : LockScreenSettingsEvents + data object OnRemovePin : LockScreenSettingsEvents data object ConfirmRemovePin : LockScreenSettingsEvents data object CancelRemovePin : LockScreenSettingsEvents data object ToggleBiometric : LockScreenSettingsEvents diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index a91597448a..9bef944194 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject class LockScreenSettingsPresenter @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val pinCodeManager: PinCodeManager, private val coroutineScope: CoroutineScope, ) : Presenter { @@ -50,7 +51,7 @@ class LockScreenSettingsPresenter @Inject constructor( mutableStateOf(false) } LaunchedEffect(triggerComputation) { - showRemovePinOption = !LockScreenConfig.IS_PIN_MANDATORY && pinCodeManager.isPinCodeAvailable() + showRemovePinOption = !lockScreenConfig.isPinMandatory && pinCodeManager.isPinCodeAvailable() } fun handleEvents(event: LockScreenSettingsEvents) { @@ -58,12 +59,14 @@ class LockScreenSettingsPresenter @Inject constructor( LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false LockScreenSettingsEvents.ConfirmRemovePin -> { coroutineScope.launch { - showRemovePinConfirmation = false - pinCodeManager.deletePinCode() - triggerComputation++ + if (showRemovePinConfirmation) { + showRemovePinConfirmation = false + pinCodeManager.deletePinCode() + triggerComputation++ + } } } - LockScreenSettingsEvents.RemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true LockScreenSettingsEvents.ToggleBiometric -> { //TODO branch biometric logic } @@ -77,5 +80,4 @@ class LockScreenSettingsPresenter @Inject constructor( eventSink = ::handleEvents ) } - } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 08234b6b07..97c640ba78 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -54,7 +54,7 @@ fun LockScreenSettingsView( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), tintColor = ElementTheme.colors.textCriticalPrimary, onClick = { - state.eventSink(LockScreenSettingsEvents.RemovePin) + state.eventSink(LockScreenSettingsEvents.OnRemovePin) } ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 06de31281c..89e8b98c51 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.delay import javax.inject.Inject class SetupPinPresenter @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val pinValidator: PinValidator, private val buildMeta: BuildMeta, private val pinCodeManager: PinCodeManager, @@ -41,10 +42,10 @@ class SetupPinPresenter @Inject constructor( @Composable override fun present(): SetupPinState { var choosePinEntry by remember { - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize)) } var confirmPinEntry by remember { - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize)) } var isConfirmationStep by remember { mutableStateOf(false) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index b164ee8c88..ec17411396 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -20,10 +20,7 @@ import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject -class PinValidator internal constructor(private val pinBlacklist: Set) { - - @Inject - constructor() : this(LockScreenConfig.PIN_BLACKLIST) +class PinValidator @Inject constructor(private val lockScreenConfig: LockScreenConfig) { sealed interface Result { data object Valid : Result @@ -32,7 +29,7 @@ class PinValidator internal constructor(private val pinBlacklist: Set) { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = pinBlacklist.any { it == pinAsText } + val isBlacklisted = lockScreenConfig.pinBlacklist.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt new file mode 100644 index 0000000000..d42dad101a --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/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.features.lockscreen.impl.fixtures + +import io.element.android.appconfig.LockScreenConfig + +internal fun aLockScreenConfig( + isPinMandatory: Boolean = false, + pinBlacklist: Set = emptySet(), + pinSize: Int = 4, + maxPinCodeAttemptsBeforeLogout: Int = 3, + gracePeriodInMillis: Long = 5 * 60 * 1000L +): LockScreenConfig { + return LockScreenConfig( + isPinMandatory = isPinMandatory, + pinBlacklist = pinBlacklist, + pinSize = pinSize, + maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout, + gracePeriodInMillis = gracePeriodInMillis + ) +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt similarity index 59% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt index a2e2dacf97..bf9ebdf541 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt @@ -14,15 +14,20 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.pin +package io.element.android.features.lockscreen.impl.fixtures +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManager +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider -internal fun createPinCodeManager(): PinCodeManager { - val pinCodeStore = InMemoryPinCodeStore() - val secretKeyProvider = SimpleSecretKeyProvider() - val encryptionDecryptionService = AESEncryptionDecryptionService() +internal fun aPinCodeManager( + pinCodeStore: PinCodeStore = InMemoryPinCodeStore(), + secretKeyProvider: SimpleSecretKeyProvider = SimpleSecretKeyProvider(), + encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(), +): PinCodeManager { return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt new file mode 100644 index 0000000000..578f20e7d9 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +import 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.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LockScreenSettingsPresenterTest { + + @Test + fun `present - remove pin flow`() = runTest { + val presenter = createLockScreenSettingsPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { state -> + state.showRemovePinOption + }.last().also { state -> + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isTrue() + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isFalse() + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isTrue() + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + } + consumeItemsUntilPredicate { + it.showRemovePinOption.not() + }.last().also { state -> + assertThat(state.showRemovePinConfirmation).isFalse() + assertThat(state.showRemovePinOption).isFalse() + } + } + } + + private suspend fun createLockScreenSettingsPresenter( + coroutineScope: CoroutineScope, + lockScreenConfig: LockScreenConfig = aLockScreenConfig(), + ): LockScreenSettingsPresenter { + val pinCodeManager = aPinCodeManager().apply { + createPinCode("1234") + } + return LockScreenSettingsPresenter( + pinCodeManager = pinCodeManager, + coroutineScope = coroutineScope, + lockScreenConfig = lockScreenConfig, + ) + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 0cc38dd355..81c37e6d69 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,8 +20,10 @@ 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.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import io.element.android.features.lockscreen.impl.pin.createPinCodeManager 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 @@ -108,9 +110,19 @@ class SetupPinPresenterTest { } } - private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter { - val pinCodeManager = createPinCodeManager() + private fun createSetupPinPresenter( + callback: PinCodeManager.Callback, + lockScreenConfig: LockScreenConfig = aLockScreenConfig( + pinBlacklist = setOf(blacklistedPin) + ), + ): SetupPinPresenter { + val pinCodeManager = aPinCodeManager() pinCodeManager.addCallback(callback) - return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta(), pinCodeManager) + return SetupPinPresenter( + lockScreenConfig = lockScreenConfig, + pinValidator = PinValidator(lockScreenConfig), + buildMeta = aBuildMeta(), + pinCodeManager = pinCodeManager + ) } } 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 index 16e44b2af0..e6ba51c537 100644 --- 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 @@ -21,7 +21,7 @@ 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.PinCodeManager -import io.element.android.features.lockscreen.impl.pin.createPinCodeManager +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel @@ -147,7 +147,7 @@ class PinUnlockPresenterTest { scope: CoroutineScope, callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, ): PinUnlockPresenter { - val pinCodeManager = createPinCodeManager().apply { + val pinCodeManager = aPinCodeManager().apply { addCallback(callback) createPinCode(completePin) } From 537068cb32dcfa65cf8300b6b7144035a2dd6f8f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 26 Oct 2023 09:32:15 +0000 Subject: [PATCH 17/22] Update screenshots --- ...ents_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ents_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...ll_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ll_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png | 3 +++ ...l.setup_null_SetupPinView-D-1_1_null_1,NEXUS_5,1.0,en].png | 3 --- ....setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png | 3 +++ ....setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-1_2_null_1,NEXUS_5,1.0,en].png | 3 --- ....setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png} | 0 ...l.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ....setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png} | 0 ....setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png} | 0 ...lock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png} | 0 ...lock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png} | 0 ...unlock_null_PinUnlockView-D-2_2_null_4,NEXUS_5,1.0,en].png | 3 --- ...nlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png} | 0 ...unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png | 3 +++ ...unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png | 3 +++ ...unlock_null_PinUnlockView-N-2_3_null_4,NEXUS_5,1.0,en].png | 3 --- ...nlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png} | 0 ...nlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png} | 0 ...unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png | 3 +++ ...unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png | 3 +++ ...ncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png | 4 ++-- ...ncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png | 4 ++-- ...ll_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...l_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...l_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png | 4 ++-- ....api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png | 4 ++-- 50 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png delete mode 100644 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 rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_1,NEXUS_5,1.0,en].png rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_2_null_4,NEXUS_5,1.0,en].png} (100%) delete mode 100644 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 rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_1,NEXUS_5,1.0,en].png rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_3,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_null_4,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 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 rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png delete mode 100644 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 rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png} (100%) rename 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 => ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png 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 index b2f7ce4747..28583d7fa6 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de1d44f62edd3f2d30421e49ad435971b165711b8bfe6d4a9475c5fb2f9f83ed -size 8506 +oid sha256:dcd61b2ec311170adfde3dc4a5e73ab216d2c9030e0b5893ff7c518488d01bb2 +size 8103 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 index a4c7739624..4899424130 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d58d2d25d8f2c07c976bcda0eea7ec101f993ad1ef733fae4c713a67650e33e2 -size 8498 +oid sha256:a5b714f3dec62d10711e977e88d536b8bd78c60eabd1a718e2f70a160e6582c2 +size 8109 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..978b895828 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55699e4df8454188d18cff9476f52ad73066b740c67b15eafaf4003cb3bee62f +size 18567 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..678036c4f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e36041959b02f320438ae5627559b52538a79e5bf813c5beec2dc30c0ba9e61 +size 20909 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bc57c63a27 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-D-1_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6ca257ae106de5d4361464132d97febacc09f211e5697d0da03c767fd8a2d05 +size 32036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dfb8d18f18 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71b5f1d9d01cd1c4ba0fc34c636fefb54099540586097d2c789befb36f69e68e +size 17124 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4fc8cff00a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2eb7e4911680f6660823a685e1fa8bf12ed623955685792dcef80ac7b25fb26 +size 19438 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2bdb7383d8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.settings_null_LockScreenSettingsView-N-1_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9968ea696d5f88103b8ae6c03c58e87799dd9f22d831614d0563b343c49e931b +size 28582 diff --git a/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 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 deleted file mode 100644 index 0c72d54e99..0000000000 --- a/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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d25ab8d3f53a5139b265a1ddef43ef3755b539a695aa8f43615e9197bd93471d -size 33231 diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_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.setup_null_SetupPinView-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-2_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_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-D-2_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a27cd2534b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-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:ae1ac3fa371e4deb0851baa75a5e68635d8ee45eaea6dd8e373768df8cf237a1 +size 32925 diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_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.setup_null_SetupPinView-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-2_2_null_2,NEXUS_5,1.0,en].png diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_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.setup_null_SetupPinView-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-2_2_null_3,NEXUS_5,1.0,en].png diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-D-2_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.setup_null_SetupPinView-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-2_2_null_4,NEXUS_5,1.0,en].png diff --git a/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 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 deleted file mode 100644 index b22bfb46dd..0000000000 --- a/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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d5037348c8b27714e54ebec53aac8a546f14b11e3195434f681aad95e416847 -size 32181 diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_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.setup_null_SetupPinView-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-2_3_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_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-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8508c0ba62 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-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:881e580f855b653e5b4159f18c948aaf5636bebca97d0f687c3bdb32bd805a46 +size 31899 diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_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.setup_null_SetupPinView-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-2_3_null_2,NEXUS_5,1.0,en].png diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_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.setup_null_SetupPinView-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-2_3_null_3,NEXUS_5,1.0,en].png diff --git a/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 b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.setup_null_SetupPinView-N-2_3_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.setup_null_SetupPinView-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-2_3_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-4_4_null,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-D-4_4_null,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-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-4_5_null,NEXUS_5,1.0,en].png similarity index 100% rename from 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 rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock.keypad_null_PinKeypad-N-4_5_null,NEXUS_5,1.0,en].png 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 deleted file mode 100644 index ee9f61a453..0000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -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-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-3_3_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.unlock_null_PinUnlockView-D-2_2_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_0,NEXUS_5,1.0,en].png 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-3_3_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.unlock_null_PinUnlockView-D-2_2_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_1,NEXUS_5,1.0,en].png 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-3_3_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.unlock_null_PinUnlockView-D-2_2_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_2,NEXUS_5,1.0,en].png 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-3_3_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.unlock_null_PinUnlockView-D-2_2_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_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-D-3_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3cc563d5a0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0d360c0697733a9e1067589be5178f631da673743b97daeb50ebdc51fd89502 +size 44289 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3cc91d32f0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-D-3_3_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc653f37c55ce7f9ef001d8f8ab57de23008baae2f644c6db49a7f4bccc831d5 +size 34938 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 deleted file mode 100644 index bb647daed5..0000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d50c34cdf50881d8176d90617c717f7214faeaa4fdce22ec4b93b0e5669b9869 -size 39073 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-3_4_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.unlock_null_PinUnlockView-N-2_3_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_0,NEXUS_5,1.0,en].png 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-3_4_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.unlock_null_PinUnlockView-N-2_3_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_1,NEXUS_5,1.0,en].png 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-3_4_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.unlock_null_PinUnlockView-N-2_3_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_2,NEXUS_5,1.0,en].png 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-3_4_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.unlock_null_PinUnlockView-N-2_3_null_3,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_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-3_4_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ac4cc89932 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5b8afd4e3adca2e42463b66b5da449caaed61ee8ba9240a8a5e93f2c7ce75b4 +size 39061 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dc79b17cbd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.lockscreen.impl.unlock_null_PinUnlockView-N-3_4_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd06f078cff08947537a58ddc2df9a79f4aa3d55372b6b8eafa962f2266ca02f +size 31443 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png index 57ff1f481f..aabcc28af3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-D-46_46_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc16dd2b27cf281c39932013a904a72bb6b417a07105d3d51b96f4b49a642e25 -size 14865 +oid sha256:38ae3e66f693072c63a13df6b016d9c29d8e15eb966d0dd43d0443a4f9e37839 +size 13396 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png index a565274d3c..9a73fd5309 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.virtual_null_TimelineEncryptedHistoryBannerView-N-46_47_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b93e0fc28c37a90991ff9778f40fc8e9b07da593d21c6bfc73451006ef6d067c -size 14415 +oid sha256:ce47e90d5cc8d336bc965ae879ab69fc1482254fbd3be524df8eb3c7b40b98df +size 13026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png index 9e6006a88a..79ac6aa584 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81ab934cdcd8e7ad76c9bce474b59fbe449960ac2abf2d7b4fa85df89270fc25 -size 46489 +oid sha256:eec57772a4c2390238b7363d91d02cc99124ca442a66a9049155f86313506b43 +size 45307 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png index 11092fbadf..a142e1a34b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewDark--1_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1a66ecf4b07ec0e57d1d5e92f9af7e4989288dec756eedef2b93d8b6b980424 -size 45815 +oid sha256:06eaf42a48e6ece74f4192de66731e4dfc23617034ddf1cbedb18eb392e3e10a +size 44638 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png index c0b3ad3ba5..5b33dfe72e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49cf3240371d9311320681520de8f44f5f8a013ba61949023a25e43b2960f513 -size 49806 +oid sha256:f860899fb9c33ab3af97dee35bcd39d9b4c128fed4c0256b2c354dc9193d1a0b +size 48393 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png index d9e8c5bdea..80cb7c6241 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.root_null_PreferencesRootViewLight--0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12afe63bad3f3586d10c4e59e9cc05ec038cc47942ebead5bed11e58b119e648 -size 49686 +oid sha256:a3eb0577c673b6da2bbf35d4eeb485c2af08cdea3e084dbf123befb2d092bbcd +size 48300 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png index 502407ff36..a578d69128 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31363274a1a4205e24640d1b02d32108afcabc62ee019da8f85442562fcf1b32 -size 32713 +oid sha256:ac49f8798ffa60757fcda059c52dffe9f16271bf507f917dffadd20e461acd2a +size 31866 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png index 1a95753f0a..cffb3cf5d8 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ba708a9862d0b67b444ec1f92e076e8d6b0be27d3d519b997c2270add4c5ace -size 31946 +oid sha256:aa94619e6f4022ee84aca4e31393de336545b7cf6d73c0a025a1cca68ce713e0 +size 31099 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png index 58dae239ef..19e14046b1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c31acff0050557fee63af7653416990e72d0b865d1f7dc8c572f197a330f891 -size 32491 +oid sha256:d0c1dff252324c71845437bd7fd261e7faacf98030365f3cca29763a1bf8ce24 +size 31645 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png index 207e50c689..956d853953 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abddf374b03a92fc98014c693cad5aca21cab5d79e781016ac761dc1e1242ec2 -size 25233 +oid sha256:fcacc94648c996c477eb4b0179df4eeffab6d1f7106cc656c548048ae33d4476 +size 24572 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png index c59c20845e..c91ef3380a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4557db65e49c39dcec5a74d44d1662bef08a7c557027f8cf0af494e8262edb9 -size 28389 +oid sha256:a7a12d96b619106be6075b39343582044062affaa58c3fe46cd04c570b08c34d +size 27650 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png index 60236462b3..54dbd92c83 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e4d70183cbb0d145850007c894bda6bec14e1ab955cecc362c4842de4455e8b -size 27548 +oid sha256:d3e6a1cf98bca50a1949cdaf27663cfbe5d2bb6f829a106f6a4291cb4b643ce1 +size 26805 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png index 8c22c300dc..27a3608686 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf1535f9bfe230c0fd8cd777ac3afefed5d1dd383a5a17eceffd466e6712477b -size 28149 +oid sha256:a7c42827f60bbe46846284cf135c15402abef93a404fc8ffa85cb27fb386d744 +size 27421 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png index 0793b6e369..0a1d99b585 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.permissions.api_null_PermissionsView-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70677ed0d1f162a17414deb276cfe7b20ea1b8a76f688b6566c67c2fe984a2ee -size 21679 +oid sha256:3775126d27af04166dac57b67e14d4f0bd0ac2baf76a00bc665bbc8e30c7c716 +size 21025 From 1e3cfcdbd60e8b326d4c9c062d29f7c3af68f090 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 13:10:25 +0200 Subject: [PATCH 18/22] PIN : Fix test compilation --- features/ftue/impl/build.gradle.kts | 1 + .../ftue/impl/DefaultFtueStateTests.kt | 4 ++ features/lockscreen/test/build.gradle.kts | 28 +++++++++++++ .../lockscreen/test/FakeLockScreenService.kt | 41 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 features/lockscreen/test/build.gradle.kts create mode 100644 features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index acdb97b517..1719aecbe1 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { testImplementation(projects.services.analytics.test) testImplementation(projects.libraries.permissions.impl) testImplementation(projects.libraries.permissions.test) + testImplementation(projects.features.lockscreen.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt index 1388eb8fc1..b84bd93d3b 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt @@ -23,6 +23,8 @@ import io.element.android.features.ftue.impl.migration.MigrationScreenStore import io.element.android.features.ftue.impl.state.DefaultFtueState import io.element.android.features.ftue.impl.state.FtueStep import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState +import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.test.FakeLockScreenService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -186,6 +188,7 @@ class DefaultFtueStateTests { migrationScreenStore: MigrationScreenStore = InMemoryMigrationScreenStore(), permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false), matrixClient: MatrixClient = FakeMatrixClient(), + lockScreenService: LockScreenService = FakeLockScreenService(), sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU, // First version where notification permission is required ) = DefaultFtueState( sdkVersionProvider = FakeBuildVersionSdkIntProvider(sdkIntVersion), @@ -194,6 +197,7 @@ class DefaultFtueStateTests { welcomeScreenState = welcomeState, migrationScreenStore = migrationScreenStore, permissionStateProvider = permissionStateProvider, + lockScreenService = lockScreenService, matrixClient = matrixClient, ) } diff --git a/features/lockscreen/test/build.gradle.kts b/features/lockscreen/test/build.gradle.kts new file mode 100644 index 0000000000..083b54b88b --- /dev/null +++ b/features/lockscreen/test/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.lockscreen.test" +} + +dependencies { + implementation(libs.coroutines.core) + api(projects.features.lockscreen.api) +} diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt new file mode 100644 index 0000000000..50581de69b --- /dev/null +++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt @@ -0,0 +1,41 @@ +/* + * 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.test + +import io.element.android.features.lockscreen.api.LockScreenLockState +import io.element.android.features.lockscreen.api.LockScreenService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeLockScreenService() : LockScreenService { + + private var isSetupRequired: Boolean = false + private val _lockState: MutableStateFlow = MutableStateFlow(LockScreenLockState.Locked) + override val lockState: StateFlow = _lockState + + override suspend fun isSetupRequired(): Boolean { + return isSetupRequired + } + + fun setIsSetupRequired(isSetupRequired: Boolean) { + this.isSetupRequired = isSetupRequired + } + + fun setLockState(lockState: LockScreenLockState) { + _lockState.value = lockState + } +} From bd9179dd45354fb0bd3efabdf6fb6cad40a6a0dd Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 14:39:13 +0200 Subject: [PATCH 19/22] PIN : Fix SetupPinPresenterTest --- .../lockscreen/impl/setup/SetupPinPresenterTest.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 81c37e6d69..5b6e76fb06 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -30,6 +30,7 @@ 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 io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import org.junit.Test @@ -77,7 +78,9 @@ class SetupPinPresenterTest { assertThat(state.setupPinFailure).isNull() state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } - awaitLastSequentialItem().also { state -> + consumeItemsUntilPredicate { + it.isConfirmationStep + }.last().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() @@ -96,7 +99,9 @@ class SetupPinPresenterTest { assertThat(state.setupPinFailure).isNull() state.eventSink(SetupPinEvents.OnPinEntryChanged(completePin)) } - awaitLastSequentialItem().also { state -> + consumeItemsUntilPredicate { + it.isConfirmationStep + }.last().also { state -> state.choosePinEntry.assertText(completePin) state.confirmPinEntry.assertEmpty() assertThat(state.isConfirmationStep).isTrue() From b8ce848a0c5dc6b1bb181fc780c3a42fb4c435e4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 15:25:40 +0200 Subject: [PATCH 20/22] PIN : fix quality --- .../android/appconfig/LockScreenConfig.kt | 1 - .../ftue/impl/state/DefaultFtueState.kt | 2 +- .../impl/DefaultLockScreenService.kt | 3 ++- .../lockscreen/impl/LockScreenFlowNode.kt | 3 ++- .../impl/pin/DefaultPinCodeManagerCallback.kt | 25 +++++++++++++++++++ .../lockscreen/impl/pin/PinCodeManager.kt | 6 ++--- .../settings/LockScreenSettingsFlowNode.kt | 3 ++- .../lockscreen/impl/unlock/PinUnlockState.kt | 5 +++- .../src/main/res/values-cs/translations.xml | 4 +++ .../src/main/res/values-de/translations.xml | 4 +++ .../src/main/res/values-es/translations.xml | 4 +++ .../src/main/res/values-fr/translations.xml | 4 +++ .../src/main/res/values-it/translations.xml | 4 +++ .../src/main/res/values-ro/translations.xml | 4 +++ .../src/main/res/values-ru/translations.xml | 4 +++ .../src/main/res/values-sk/translations.xml | 10 +++++++- .../main/res/values-zh-rTW/translations.xml | 4 +++ .../impl/setup/SetupPinPresenterTest.kt | 3 ++- .../impl/unlock/PinUnlockPresenterTest.kt | 9 ++++--- .../lockscreen/test/FakeLockScreenService.kt | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values-ru/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 2 +- .../main/res/values-zh-rTW/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-sk/translations.xml | 14 ++++++++--- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values-ru/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 3 ++- .../main/res/values-zh-rTW/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values-ru/translations.xml | 2 +- .../src/main/res/values-sk/translations.xml | 2 +- .../main/res/values-zh-rTW/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../impl/KeyStoreSecretKeyProvider.kt | 4 ++- .../main/res/values-zh-rTW/translations.xml | 1 + .../src/main/res/values-sk/translations.xml | 3 +++ .../src/main/res/values/localazy.xml | 1 + 49 files changed, 131 insertions(+), 45 deletions(-) create mode 100644 features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt create mode 100644 features/lockscreen/impl/src/main/res/values-cs/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-de/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-es/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-fr/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-it/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-ro/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-ru/translations.xml create mode 100644 features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index e73b3cd150..5f72bc6f86 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -63,5 +63,4 @@ object LockScreenConfigModule { maxPinCodeAttemptsBeforeLogout = 3, gracePeriodInMillis = 90_000L ) - } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt index 07d2372344..7d80fd7413 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt @@ -93,7 +93,7 @@ class DefaultFtueState @Inject constructor( { shouldAskNotificationPermissions() }, { needsAnalyticsOptIn() }, { shouldDisplayLockscreenSetup() }, - ).any { it -> it() } + ).any { it() } } private fun shouldDisplayMigrationScreen(): Boolean { diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index 32e1e3d1b9..af1c40ffb7 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn @@ -53,7 +54,7 @@ class DefaultLockScreenService @Inject constructor( private var lockJob: Job? = null init { - pinCodeManager.addCallback(object : PinCodeManager.Callback { + pinCodeManager.addCallback(object : DefaultPinCodeManagerCallback() { override fun onPinCodeVerified() { _lockScreenState.value = LockScreenLockState.Unlocked } 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 e10ca406fb..a5560a5179 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 @@ -30,6 +30,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.lockscreen.api.LockScreenEntryPoint +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.settings.LockScreenSettingsFlowNode import io.element.android.features.lockscreen.impl.setup.SetupPinNode @@ -70,7 +71,7 @@ class LockScreenFlowNode @AssistedInject constructor( data object Settings : NavTarget } - private val pinCodeManagerCallback = object : PinCodeManager.Callback { + private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { plugins().forEach { it.onSetupCompleted() diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt new file mode 100644 index 0000000000..3ce8565cd2 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.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.pin + +open class DefaultPinCodeManagerCallback : PinCodeManager.Callback { + override fun onPinCodeVerified() = Unit + + override fun onPinCodeCreated() = Unit + + override fun onPinCodeRemoved() = Unit +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt index c214e533ab..21e7281dc8 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt @@ -29,17 +29,17 @@ interface PinCodeManager { /** * Called when the pin code is verified. */ - fun onPinCodeVerified() = Unit + fun onPinCodeVerified() /** * Called when the pin code is created. */ - fun onPinCodeCreated() = Unit + fun onPinCodeCreated() /** * Called when the pin code is removed. */ - fun onPinCodeRemoved() = Unit + fun onPinCodeRemoved() } /** diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index 63d1937317..a07607eea3 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -32,6 +32,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.setup.SetupPinNode import io.element.android.features.lockscreen.impl.unlock.PinUnlockNode @@ -70,7 +71,7 @@ class LockScreenSettingsFlowNode @AssistedInject constructor( data object Settings : NavTarget } - private val pinCodeManagerCallback = object : PinCodeManager.Callback { + private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeVerified() { backstack.newRoot(NavTarget.Settings) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt index 178e8692d1..29d246b21b 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockState.kt @@ -27,5 +27,8 @@ data class PinUnlockState( val signOutAction: Async, val eventSink: (PinUnlockEvents) -> Unit ) { - val isSignOutPromptCancellable = (remainingAttempts.dataOrNull() ?: 0) > 0 + val isSignOutPromptCancellable = when (remainingAttempts) { + is Async.Success -> remainingAttempts.data > 0 + else -> true + } } diff --git a/features/lockscreen/impl/src/main/res/values-cs/translations.xml b/features/lockscreen/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..0f7f3decfb --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,4 @@ + + + "Odhlašování…" + diff --git a/features/lockscreen/impl/src/main/res/values-de/translations.xml b/features/lockscreen/impl/src/main/res/values-de/translations.xml new file mode 100644 index 0000000000..9491e2e1a0 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-de/translations.xml @@ -0,0 +1,4 @@ + + + "Abmelden…" + diff --git a/features/lockscreen/impl/src/main/res/values-es/translations.xml b/features/lockscreen/impl/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..5f7393df00 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "Cerrando sesión…" + diff --git a/features/lockscreen/impl/src/main/res/values-fr/translations.xml b/features/lockscreen/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..64944ff843 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,4 @@ + + + "Déconnexion…" + diff --git a/features/lockscreen/impl/src/main/res/values-it/translations.xml b/features/lockscreen/impl/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..579346ed6e --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Uscita in corso…" + diff --git a/features/lockscreen/impl/src/main/res/values-ro/translations.xml b/features/lockscreen/impl/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..7cbd3ca512 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Deconectare în curs…" + diff --git a/features/lockscreen/impl/src/main/res/values-ru/translations.xml b/features/lockscreen/impl/src/main/res/values-ru/translations.xml new file mode 100644 index 0000000000..3ce820fc0c --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-ru/translations.xml @@ -0,0 +1,4 @@ + + + "Выполняется выход…" + diff --git a/features/lockscreen/impl/src/main/res/values-sk/translations.xml b/features/lockscreen/impl/src/main/res/values-sk/translations.xml index 593542d81e..fd4d8f6da7 100644 --- a/features/lockscreen/impl/src/main/res/values-sk/translations.xml +++ b/features/lockscreen/impl/src/main/res/values-sk/translations.xml @@ -1,16 +1,24 @@ + + "Máte 3 pokusy na odomknutie" + "Nesprávny PIN kód. Máte ešte %1$d pokus" "Nesprávny PIN kód. Máte ešte %1$d pokusy" "Nesprávny PIN kód. Máte ešte %1$d pokusov" + "biometrické overenie" + "biometrické odomknutie" "Zabudli ste PIN?" "Zmeniť PIN kód" "Povoliť biometrické odomknutie" "Odstrániť PIN" "Ste si istí, že chcete odstrániť PIN?" "Odstrániť PIN?" + "Povoliť %1$s" + "Radšej použijem PIN" + "Ušetrite si čas a použite zakaždým %1$s na odomknutie aplikácie" "Vyberte PIN" "Potvrdiť PIN" "Z bezpečnostných dôvodov si nemôžete toto zvoliť ako svoj PIN kód." @@ -22,5 +30,5 @@ Vyberte si niečo zapamätateľné. Ak tento kód PIN zabudnete, budete z aplik "PIN kódy sa nezhodujú" "Ak chcete pokračovať, musíte sa znovu prihlásiť a vytvoriť nový PIN kód." "Prebieha odhlasovanie" - "Máte 3 pokusy na odomknutie" + "Prebieha odhlasovanie…" diff --git a/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml new file mode 100644 index 0000000000..9d0c11fcd8 --- /dev/null +++ b/features/lockscreen/impl/src/main/res/values-zh-rTW/translations.xml @@ -0,0 +1,4 @@ + + + "正在登出…" + diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 5b6e76fb06..3969ea7c28 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText @@ -45,7 +46,7 @@ class SetupPinPresenterTest { @Test fun `present - complete flow`() = runTest { val pinCodeCreated = CompletableDeferred() - val callback = object : PinCodeManager.Callback { + val callback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { pinCodeCreated.complete(Unit) } 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 index e6ba51c537..234beae337 100644 --- 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 @@ -20,8 +20,9 @@ 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.PinCodeManager import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManagerCallback +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel @@ -42,7 +43,7 @@ class PinUnlockPresenterTest { @Test fun `present - success verify flow`() = runTest { val pinCodeVerified = CompletableDeferred() - val callback = object : PinCodeManager.Callback { + val callback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { pinCodeVerified.complete(Unit) } @@ -82,7 +83,7 @@ class PinUnlockPresenterTest { @Test fun `present - failure verify flow`() = runTest { val pinCodeVerified = CompletableDeferred() - val callback = object : PinCodeManager.Callback { + val callback = object : DefaultPinCodeManagerCallback() { override fun onPinCodeCreated() { pinCodeVerified.complete(Unit) } @@ -145,7 +146,7 @@ class PinUnlockPresenterTest { private suspend fun createPinUnlockPresenter( scope: CoroutineScope, - callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, + callback: PinCodeManager.Callback = DefaultPinCodeManagerCallback(), ): PinUnlockPresenter { val pinCodeManager = aPinCodeManager().apply { addCallback(callback) diff --git a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt index 50581de69b..012c8e9a5c 100644 --- a/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt +++ b/features/lockscreen/test/src/main/kotlin/io/element/android/features/lockscreen/test/FakeLockScreenService.kt @@ -21,7 +21,7 @@ import io.element.android.features.lockscreen.api.LockScreenService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeLockScreenService() : LockScreenService { +class FakeLockScreenService : LockScreenService { private var isSetupRequired: Boolean = false private val _lockState: MutableStateFlow = MutableStateFlow(LockScreenLockState.Locked) diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index 3edf7d1003..0094479b12 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -35,8 +35,8 @@ "Na %2$s je momentálně vysoká poptávka po %1$s. Vraťte se do aplikace za pár dní a zkuste to znovu. Díky za trpělivost!" - "Vítá vás %1$s" "Jste v pořadníku!" "Jdete do toho!" "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." + "Vítá vás %1$s!" diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index ac7df075db..0e757190a9 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -34,8 +34,8 @@ "Derzeit besteht eine hohe Nachfrage nach %1$s auf %2$s. Kehre in ein paar Tagen zur App zurück und versuche es erneut. Danke für deine Geduld!" - "Willkommen bei %1$s!" "Du bist fast am Ziel." "Du bist dabei." "Matrix ist ein offenes Netzwerk für eine sichere, dezentrale Kommunikation." + "Willkommen bei %1$s!" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 3b7dae468c..7362aac3e8 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -34,8 +34,8 @@ "Il y a une forte demande pour %1$s sur %2$s à l’heure actuelle. Revenez sur l’application dans quelques jours et réessayez. Merci pour votre patience !" - "Bienvenue dans %1$s !" "Vous y êtes presque." "Vous y êtes." "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée." + "Bienvenue dans %1$s !" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 2241c63ec9..49e701558a 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -34,8 +34,8 @@ "Există o cerere mare pentru %1$s pentru %2$s în acest moment. Reveniți la aplicație în câteva zile și încercați din nou. Vă mulțumim pentru răbdare!" - "Bun venit la %1$s" "Sunteți pe lista de așteptare" "Sunteți conectat!" "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." + "Bun venit la%1$s!" diff --git a/features/login/impl/src/main/res/values-ru/translations.xml b/features/login/impl/src/main/res/values-ru/translations.xml index 733b0d93d2..1751364597 100644 --- a/features/login/impl/src/main/res/values-ru/translations.xml +++ b/features/login/impl/src/main/res/values-ru/translations.xml @@ -35,8 +35,8 @@ "В настоящее время существует высокий спрос на %1$s на %2$s. Вернитесь в приложение через несколько дней и попробуйте снова. Спасибо за терпение!" - "Добро пожаловать в %1$s!" "Почти готово!" "Вы зарегистрированы!" "Matrix — это открытая сеть для безопасной децентрализованной связи." + "Добро пожаловать в %1$s!" diff --git a/features/login/impl/src/main/res/values-sk/translations.xml b/features/login/impl/src/main/res/values-sk/translations.xml index 31656d628a..ef7ba0d8dc 100644 --- a/features/login/impl/src/main/res/values-sk/translations.xml +++ b/features/login/impl/src/main/res/values-sk/translations.xml @@ -35,8 +35,8 @@ "Momentálne je veľký dopyt po %1$s na %2$s. Vráťte sa do aplikácie za pár dní a skúste to znova. Ďakujeme za trpezlivosť!" - "Vitajte v %1$s" "Ste na čakanej listine!" "Ste dnu!" "Matrix je otvorená sieť pre bezpečnú a decentralizovanú komunikáciu." + "Vitajte v %1$s!" diff --git a/features/login/impl/src/main/res/values-zh-rTW/translations.xml b/features/login/impl/src/main/res/values-zh-rTW/translations.xml index 45f10295f3..e482fcc9f1 100644 --- a/features/login/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/login/impl/src/main/res/values-zh-rTW/translations.xml @@ -25,6 +25,6 @@ "您的所有對話將保存於此,就如同您的電子郵件供應商會保存您的電子郵件一樣。" "您即將登入 %1$s" "您即將在 %1$s 建立帳號" - "歡迎使用 %1$s!" "Matrix 是一個開放網路,為了安全、去中心化的通訊而生。" + "歡迎使用 %1$s!" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index c9797db5ac..2149318670 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -35,8 +35,8 @@ "There\'s a high demand for %1$s on %2$s at the moment. Come back to the app in a few days and try again. Thanks for your patience!" - "Welcome to %1$s!" "You’re almost there." "You\'re in." "Matrix is an open network for secure, decentralised communication." + "Welcome to %1$s!" diff --git a/features/logout/api/src/main/res/values-sk/translations.xml b/features/logout/api/src/main/res/values-sk/translations.xml index 2c334e9c3b..fcd78f2022 100644 --- a/features/logout/api/src/main/res/values-sk/translations.xml +++ b/features/logout/api/src/main/res/values-sk/translations.xml @@ -1,12 +1,18 @@ - "Prosím, počkajte na dokončenie tohto kroku a až potom sa odhláste." - "Vaše kľúče sa ešte stále zálohujú" "Ste si istí, že sa chcete odhlásiť?" "Odhlásiť sa" "Prebieha odhlasovanie…" - "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." - "Uložili ste kľúč na obnovenie?" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." + "Vypli ste zálohovanie" + "Keď ste sa odpojili od internetu, vaše kľúče sa ešte stále zálohovali. Pripojte sa znova k internetu, aby sa vaše kľúče mohli zálohovať pred odhlásením." + "Vaše kľúče sa ešte stále zálohujú" + "Pred odhlásením počkajte, kým sa to dokončí." + "Vaše kľúče sa ešte stále zálohujú" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, stratíte prístup k svojim šifrovaným správam." + "Obnovenie nie je nastavené" + "Chystáte sa odhlásiť z vašej poslednej relácie. Ak sa teraz odhlásite, môžete stratiť prístup k svojim šifrovaným správam." + "Uložili ste si kľúč na obnovenie?" "Odhlásiť sa" "Odhlásiť sa" diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index 7129031292..3bd09a0b76 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -30,7 +30,6 @@ "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu." "Nastavení režimu se nezdařilo, zkuste to prosím znovu." "Všechny zprávy" - "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "Zobrazit méně" "Zobrazit více" @@ -40,4 +39,5 @@ "Zobrazit méně" "Držte pro nahrávání" "Nahrání média se nezdařilo, zkuste to prosím znovu." + "Pouze zmínky a klíčová slova" diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index d08c04a785..161868614c 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -29,7 +29,6 @@ "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Alle Nachrichten" - "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich in diesem Raum bei" "Weniger anzeigen" "Mehr anzeigen" @@ -38,4 +37,5 @@ "Emoji hinzufügen" "Weniger anzeigen" "Fehler beim Verarbeiten des hochgeladenen Mediums. Bitte versuche es erneut." + "Nur Erwähnungen und Schlüsselwörter" diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index ea6f387d1a..aed47e5ff8 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -29,7 +29,6 @@ "Échec de la restauration du mode par défaut, veuillez réessayer." "Échec de la configuration du mode, veuillez réessayer." "Tous les messages" - "Mentions et mots clés uniquement" "Dans ce salon, prévenez-moi pour" "Afficher moins" "Afficher plus" @@ -38,4 +37,5 @@ "Ajouter un émoji" "Afficher moins" "Échec du traitement des médias à télécharger, veuillez réessayer." + "Mentions et mots clés uniquement" diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 20bec7b1b3..16e4867196 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -30,7 +30,6 @@ "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." "Toate mesajele" - "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "Afișați mai puțin" "Afișați mai mult" @@ -39,4 +38,5 @@ "Adăugați emoji" "Afișați mai puțin" "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." + "Numai mențiuni și cuvinte cheie" diff --git a/features/messages/impl/src/main/res/values-ru/translations.xml b/features/messages/impl/src/main/res/values-ru/translations.xml index 4bb44db372..2ba01ab763 100644 --- a/features/messages/impl/src/main/res/values-ru/translations.xml +++ b/features/messages/impl/src/main/res/values-ru/translations.xml @@ -30,7 +30,6 @@ "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." "Все сообщения" - "Только упоминания и ключевые слова" "В этой комнате уведомить меня о" "Показать меньше" "Показать больше" @@ -40,4 +39,5 @@ "Показать меньше" "Удерживайте для записи" "Не удалось обработать медиафайл для загрузки, попробуйте еще раз." + "Только упоминания и ключевые слова" diff --git a/features/messages/impl/src/main/res/values-sk/translations.xml b/features/messages/impl/src/main/res/values-sk/translations.xml index c9129faf08..7b89350ff8 100644 --- a/features/messages/impl/src/main/res/values-sk/translations.xml +++ b/features/messages/impl/src/main/res/values-sk/translations.xml @@ -14,6 +14,7 @@ "Anketa" "Formátovanie textu" "História správ v tejto miestnosti nie je momentálne k dispozícii" + "História správ nie je v tejto miestnosti k dispozícii. Ak chcete zobraziť históriu správ, overte toto zariadenie." "Nepodarilo sa získať údaje o používateľovi" "Chceli by ste ich pozvať späť?" "V tomto rozhovore ste sami" @@ -30,7 +31,6 @@ "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Všetky správy" - "Iba zmienky a kľúčové slová" "V tejto miestnosti ma upozorniť na" "Zobraziť menej" "Zobraziť viac" @@ -40,4 +40,5 @@ "Zobraziť menej" "Podržaním nahrajte" "Nepodarilo sa spracovať médiá na odoslanie, skúste to prosím znova." + "Iba zmienky a kľúčové slová" diff --git a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml index 078d71bd9f..d3e9c9bce9 100644 --- a/features/messages/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/messages/impl/src/main/res/values-zh-rTW/translations.xml @@ -19,11 +19,11 @@ "無法重設為預設模式,請再試一次。" "無法設定模式,請再試一次。" "所有訊息" - "僅限提及與關鍵字" "較少" "更多" "重傳" "無法傳送您的訊息" "新增表情符號" "較少" + "僅限提及與關鍵字" diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 555ff574df..233a9ecbf4 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -30,7 +30,6 @@ "Failed restoring the default mode, please try again." "Failed setting the mode, please try again." "All messages" - "Mentions and Keywords only" "In this room, notify me for" "Show less" "Show more" @@ -40,4 +39,5 @@ "Show less" "Hold to record" "Failed processing media to upload, please try again." + "Mentions and Keywords only" diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml index 7c80ba3066..b17ef35e9b 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -36,7 +36,6 @@ "Obnovení výchozího režimu se nezdařilo, zkuste to prosím znovu." "Nastavení režimu se nezdařilo, zkuste to prosím znovu." "Všechny zprávy" - "Pouze zmínky a klíčová slova" "V této místnosti mě upozornit na" "Zablokovat" "Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat." @@ -47,4 +46,5 @@ "Opustit místnost" "Zabezpečení" "Téma" + "Pouze zmínky a klíčová slova" diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index 9549709ef0..5f0d558820 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -35,7 +35,6 @@ "Fehler beim Wiederherstellen des Standardmodus. Bitte versuche es erneut." "Fehler beim Einstellen des Modus. Bitte versuche es erneut." "Alle Nachrichten" - "Nur Erwähnungen und Schlüsselwörter" "Benachrichtige mich in diesem Raum bei" "Sperren" "Gesperrte Benutzer können dir keine Nachrichten senden und alle ihre Nachrichten werden ausgeblendet. Du kannst sie jederzeit entsperren." @@ -46,4 +45,5 @@ "Raum verlassen" "Sicherheit" "Thema" + "Nur Erwähnungen und Schlüsselwörter" diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml index 7cf727dfb5..f899a58873 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -35,7 +35,6 @@ "Échec de la restauration du mode par défaut, veuillez réessayer." "Échec de la configuration du mode, veuillez réessayer." "Tous les messages" - "Mentions et mots clés uniquement" "Dans ce salon, prévenez-moi pour" "Bloquer" "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment." @@ -46,4 +45,5 @@ "Quitter le salon" "Sécurité" "Sujet" + "Mentions et mots clés uniquement" diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index 7ec53722f4..958058432a 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -35,7 +35,6 @@ "Nu s-a reușit restaurarea modului implicit, vă rugăm să încercați din nou." "Nu s-a reușit setarea modului, vă rugăm să încercați din nou." "Toate mesajele" - "Numai mențiuni și cuvinte cheie" "În această cameră, anunțați-mă pentru" "Blocați" "Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând." @@ -46,4 +45,5 @@ "Părăsiți camera" "Securitate" "Subiect" + "Numai mențiuni și cuvinte cheie" diff --git a/features/roomdetails/impl/src/main/res/values-ru/translations.xml b/features/roomdetails/impl/src/main/res/values-ru/translations.xml index 1b2a305d3a..920fb1a2f8 100644 --- a/features/roomdetails/impl/src/main/res/values-ru/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ru/translations.xml @@ -36,7 +36,6 @@ "Не удалось восстановить режим по умолчанию, попробуйте еще раз." "Не удалось настроить режим, попробуйте еще раз." "Все сообщения" - "Только упоминания и ключевые слова" "В этой комнате уведомить меня о" "Заблокировать" "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время." @@ -47,4 +46,5 @@ "Покинуть комнату" "Безопасность" "Тема" + "Только упоминания и ключевые слова" diff --git a/features/roomdetails/impl/src/main/res/values-sk/translations.xml b/features/roomdetails/impl/src/main/res/values-sk/translations.xml index 8d28fe5fe5..5d6a4131b5 100644 --- a/features/roomdetails/impl/src/main/res/values-sk/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-sk/translations.xml @@ -36,7 +36,6 @@ "Nepodarilo sa obnoviť predvolený režim, skúste to prosím znova." "Nepodarilo sa nastaviť režim, skúste to prosím znova." "Všetky správy" - "Iba zmienky a kľúčové slová" "V tejto miestnosti ma upozorniť na" "Zablokovať" "Blokovaní používatelia vám nebudú môcť posielať správy a všetky ich správy budú skryté. Môžete ich kedykoľvek odblokovať." @@ -47,4 +46,5 @@ "Opustiť miestnosť" "Bezpečnosť" "Téma" + "Iba zmienky a kľúčové slová" diff --git a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml index 4b82111269..71ed1df861 100644 --- a/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-zh-rTW/translations.xml @@ -26,7 +26,6 @@ "無法重設為預設模式,請再試一次。" "無法設定模式,請再試一次。" "所有訊息" - "僅限提及與關鍵字" "封鎖" "封鎖使用者" "解除封鎖" @@ -34,4 +33,5 @@ "離開聊天室" "安全性" "主題" + "僅限提及與關鍵字" diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index c88e2e43fa..833b06ca40 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -35,7 +35,6 @@ "Failed restoring the default mode, please try again." "Failed setting the mode, please try again." "All messages" - "Mentions and Keywords only" "In this room, notify me for" "Block" "Blocked users won\'t be able to send you messages and all their messages will be hidden. You can unblock them anytime." @@ -46,4 +45,5 @@ "Leave room" "Security" "Topic" + "Mentions and Keywords only" diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt index 12be896a5b..9313e7a48b 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyProvider.kt @@ -40,7 +40,9 @@ class KeyStoreSecretKeyProvider @Inject constructor() : SecretKeyProvider { // False positive lint issue @SuppressLint("WrongConstant") override fun getOrCreateKey(alias: String): SecretKey { - val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).also { it.load(null) } + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { + load(null) + } val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry) ?.secretKey return if (secretKeyEntry == null) { diff --git a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml index 2453e2d825..0cee629b87 100644 --- a/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-zh-rTW/translations.xml @@ -30,6 +30,7 @@ "%1$s 已被您移除" "%1$s 邀請 %2$s 加入聊天室" "您邀請 %1$s 加入聊天室" + "%1$s 撤銷了 %2$s 加入房間的邀請" "%1$s 將主題變更為 %2$s" "您將主題變更為 %1$s" "聊天室主題已被 %1$s 移除" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index 5cb20f8e22..ccefa660f2 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -167,6 +167,7 @@ "Video" "Hlasová správa" "Čaká sa…" + "Čaká sa na dešifrovací kľúč" "Ste si istí, že chcete ukončiť túto anketu?" "Anketa: %1$s" "Potvrdenie" @@ -273,6 +274,8 @@ Ak budete pokračovať, niektoré z vašich nastavení sa môžu zmeniť.""Zadať…" "Kľúč na obnovu potvrdený" "Potvrďte kľúč na obnovenie" + "Skopírovaný kľúč na obnovenie" + "Generovanie…" "Uložiť kľúč na obnovenie" "Zapíšte si kľúč na obnovenie na bezpečné miesto alebo ho uložte do správcu hesiel." "Ťuknutím skopírujte kľúč na obnovenie" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index be27e21ff4..dc8d916346 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -34,6 +34,7 @@ "Edit" "Enable" "End poll" + "Enter PIN" "Forgot password?" "Forward" "Invite" From 4b36eb0374aa21c72efd043e8079b915dbcbe936 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 26 Oct 2023 15:28:18 +0200 Subject: [PATCH 21/22] PIN: address PR review --- .../impl/components/PinEntryTextField.kt | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) 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 index e4869e71d8..019236aba7 100644 --- 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 @@ -20,8 +20,10 @@ 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.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -118,10 +120,19 @@ private fun PinDigitView( @Composable internal fun PinEntryTextFieldPreview() { ElementPreview { - PinEntryTextField( - pinEntry = PinEntry.createEmpty(4).fillWith("12"), - isSecured = true, - onValueChange = {}, - ) + val pinEntry = PinEntry.createEmpty(4).fillWith("12") + Column { + PinEntryTextField( + pinEntry = pinEntry, + isSecured = true, + onValueChange = {}, + ) + Spacer(modifier = Modifier.size(16.dp)) + PinEntryTextField( + pinEntry = pinEntry, + isSecured = false, + onValueChange = {}, + ) + } } } From 27d4cab217efb74244bc8aae7516fa76bbc405bd Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 26 Oct 2023 13:45:59 +0000 Subject: [PATCH 22/22] Update screenshots --- ...ents_null_PinEntryTextField-D-0_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ents_null_PinEntryTextField-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index 28583d7fa6..6e3251fcd2 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dcd61b2ec311170adfde3dc4a5e73ab216d2c9030e0b5893ff7c518488d01bb2 -size 8103 +oid sha256:079fff99063ebce93c14b1c373ead6eba3a5952610c3c43429da597ae3a804b1 +size 11928 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 index 4899424130..70a36a4bf3 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5b714f3dec62d10711e977e88d536b8bd78c60eabd1a718e2f70a160e6582c2 -size 8109 +oid sha256:db3f1674750094cef7b8e81a462ee4d2d180a2978dfa682f9db74bac5ad1a40d +size 11939