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 9427a1f9c7..5f72bc6f86 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -16,20 +16,51 @@ 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. + */ + val maxPinCodeAttemptsBeforeLogout: Int, + + /** + * Time period before locking the app once backgrounded. + */ + 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/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 8d2edd0843..c1eb17dec1 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.lockscreen.api.LockScreenEntryPoint +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.lockscreen.api.LockScreenEntryPoint -import io.element.android.features.lockscreen.api.LockScreenState -import io.element.android.features.lockscreen.api.LockScreenStateService import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint @@ -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. @@ -220,7 +210,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 { @@ -357,9 +349,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, @@ -371,7 +363,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..1719aecbe1 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) @@ -57,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/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..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 @@ -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,17 @@ class FtueFlowNode @AssistedInject constructor( NavTarget.AnalyticsOptIn -> { 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() + } } } @@ -156,6 +172,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..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 @@ -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() } } 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/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/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..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 @@ -16,6 +16,28 @@ 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 com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint -interface LockScreenEntryPoint : SimpleFeatureEntryPoint +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, + Unlock + } +} 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/LockScreenService.kt b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt new file mode 100644 index 0000000000..c6fe444119 --- /dev/null +++ b/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenService.kt @@ -0,0 +1,32 @@ +/* + * 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.api + +import kotlinx.coroutines.flow.StateFlow + +interface LockScreenService { + /** + * The current lock state of the app. + */ + val lockState: StateFlow + + /** + * 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..fdb9dcd178 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -43,6 +43,9 @@ 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) + implementation(libs.androidx.datastore.preferences) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -54,4 +57,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/DefaultLockScreenEntryPoint.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenEntryPoint.kt index 736be374cd..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 @@ -27,7 +27,34 @@ 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 + 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 + } + + 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 + } + ) + 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/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt new file mode 100644 index 0000000000..af1c40ffb7 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -0,0 +1,114 @@ +/* + * 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.DefaultPinCodeManagerCallback +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 lockScreenConfig: LockScreenConfig, + 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 : DefaultPinCodeManagerCallback() { + override fun onPinCodeVerified() { + _lockScreenState.value = LockScreenLockState.Unlocked + } + + override fun onPinCodeRemoved() { + _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.gracePeriodInMillis) + } + } + } + } + + override suspend fun isSetupRequired(): Boolean { + return lockScreenConfig.isPinMandatory + && 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 fa3b88e18a..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 @@ -20,40 +20,75 @@ 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.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 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 +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, + private val pinCodeManager: PinCodeManager, ) : 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 + } + + private val pinCodeManagerCallback = object : DefaultPinCodeManagerCallback() { + 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 { @@ -64,6 +99,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/components/PinEntryTextField.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/components/PinEntryTextField.kt index 91f6d435c5..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,7 +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.Row +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 @@ -30,7 +33,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,34 +44,37 @@ import io.element.android.libraries.theme.ElementTheme @Composable fun PinEntryTextField( pinEntry: PinEntry, + isSecured: Boolean, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, ) { 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) + PinEntryRow(pinEntry = pinEntry, isSecured = isSecured) } ) } +@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) + PinDigitView(digit = digit, isSecured = isSecured) } } } @@ -77,6 +82,7 @@ private fun PinEntryRow( @Composable private fun PinDigitView( digit: PinDigit, + isSecured: Boolean, modifier: Modifier = Modifier, ) { val shape = RoundedCornerShape(8.dp) @@ -96,8 +102,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 ) } @@ -109,9 +120,19 @@ private fun PinDigitView( @Composable internal fun PinEntryTextFieldPreview() { ElementPreview { - PinEntryTextField( - pinEntry = PinEntry.createEmpty(4).fillWith("12"), - 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 = {}, + ) + } } } 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..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 @@ -22,25 +22,46 @@ 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() } + 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() pinCodeStore.saveEncryptedPinCode(encryptedPinCode) + callbacks.forEach { it.onPinCodeCreated() } } override suspend fun verifyPinCode(pinCode: String): Boolean { @@ -48,7 +69,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 +87,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/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt similarity index 69% rename from features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt index 2f2e6b2376..3ce8565cd2 100644 --- a/features/lockscreen/api/src/main/kotlin/io/element/android/features/lockscreen/api/LockScreenStateService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/DefaultPinCodeManagerCallback.kt @@ -14,14 +14,12 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.api +package io.element.android.features.lockscreen.impl.pin -import kotlinx.coroutines.flow.StateFlow +open class DefaultPinCodeManagerCallback : PinCodeManager.Callback { + override fun onPinCodeVerified() = Unit -interface LockScreenStateService { - val state: StateFlow + override fun onPinCodeCreated() = Unit - suspend fun entersForeground() - suspend fun entersBackground() - suspend fun unlock() + 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 5f84f5296d..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 @@ -21,11 +21,47 @@ 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() + + /** + * Called when the pin code is created. + */ + fun onPinCodeCreated() + + /** + * Called when the pin code is removed. + */ + fun onPinCodeRemoved() + } + + /** + * 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. */ 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 @@ -46,16 +82,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/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/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..8631c05502 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -0,0 +1,92 @@ +/* + * 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, + private val lockScreenConfig: LockScreenConfig, +) : 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.getRemainingPinCodeAttemptsNumber() + }.first() + } + + override suspend fun onWrongPin() { + context.dataStore.edit { preferences -> + val current = preferences.getRemainingPinCodeAttemptsNumber() + val remaining = (current - 1).coerceAtLeast(0) + preferences[remainingAttemptsKey] = remaining + } + } + + override suspend fun resetCounter() { + context.dataStore.edit { preferences -> + preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout + } + } + + 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() + } + + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout +} 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 27f4636400..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/SharedPreferencesPinCodeStore.kt +++ /dev/null @@ -1,104 +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.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -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" -private const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class SharedPreferencesPinCodeStore @Inject constructor( - private val dispatchers: CoroutineDispatchers, - 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) { - // Also reset the counters - resetCounter() - 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) { - mutex.withLock { - sharedPreferences.getInt(REMAINING_PIN_CODE_ATTEMPTS_KEY, MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT) - } - } - - override suspend fun onWrongPin(): Int = withContext(dispatchers.io) { - mutex.withLock { - val remaining = getRemainingPinCodeAttemptsNumber() - 1 - 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/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..9032e8d0ef --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.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 + +sealed interface 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/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt new file mode 100644 index 0000000000..a07607eea3 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -0,0 +1,134 @@ +/* + * 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.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 +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.SessionScope +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::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 : DefaultPinCodeManagerCallback() { + 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 new file mode 100644 index 0000000000..96f5393483 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsNode.kt @@ -0,0 +1,55 @@ +/* + * 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 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.SessionScope + +@ContributesNode(SessionScope::class) +class LockScreenSettingsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + 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, + 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 new file mode 100644 index 0000000000..9bef944194 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -0,0 +1,83 @@ +/* + * 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.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( + private val lockScreenConfig: LockScreenConfig, + 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.isPinMandatory && pinCodeManager.isPinCodeAvailable() + } + + fun handleEvents(event: LockScreenSettingsEvents) { + when (event) { + LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false + LockScreenSettingsEvents.ConfirmRemovePin -> { + coroutineScope.launch { + if (showRemovePinConfirmation) { + showRemovePinConfirmation = false + pinCodeManager.deletePinCode() + triggerComputation++ + } + } + } + LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvents.ToggleBiometric -> { + //TODO branch biometric logic + } + } + } + + return LockScreenSettingsState( + 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 new file mode 100644 index 0000000000..a6697d8a6d --- /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 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 new file mode 100644 index 0000000000..b693b033b1 --- /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( + 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 new file mode 100644 index 0000000000..97c640ba78 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -0,0 +1,90 @@ +/* + * 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, + 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 = onChangePinClicked + ) + PreferenceDivider() + if (state.showRemovePinOption) { + PreferenceText( + title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), + tintColor = ElementTheme.colors.textCriticalPrimary, + onClick = { + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + ) + } + 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 = state, + onChangePinClicked = {}, + onBackPressed = {}, + ) + } +} 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..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, @@ -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/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 3c380e6be7..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 @@ -17,30 +17,35 @@ 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 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 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( + private val lockScreenConfig: LockScreenConfig, private val pinValidator: PinValidator, private val buildMeta: BuildMeta, + private val pinCodeManager: PinCodeManager, ) : Presenter { @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) @@ -48,29 +53,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) { + pinCodeManager.createPinCode(confirmPinEntry.toText()) + } 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 -> { 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..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 @@ -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,18 @@ private fun SetupPinContent( state: SetupPinState, modifier: Modifier = Modifier, ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } PinEntryTextField( - state.activePinEntry, + pinEntry = state.activePinEntry, + isSecured = true, onValueChange = { state.eventSink(SetupPinEvents.OnPinEntryChanged(it)) }, modifier = modifier + .focusRequester(focusRequester) .padding(top = 36.dp) .fillMaxWidth() ) 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/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 f2e037b111..0000000000 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/state/DefaultLockScreenStateService.kt +++ /dev/null @@ -1,64 +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.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.Job -import kotlinx.coroutines.coroutineScope -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, -) : LockScreenStateService { - - private val _lockScreenState = MutableStateFlow(LockScreenState.Unlocked) - override val state: StateFlow = _lockScreenState - - private var lockJob: Job? = null - - override suspend fun unlock() { - if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - _lockScreenState.value = LockScreenState.Unlocked - } - } - - override suspend fun entersForeground() { - lockJob?.cancel() - } - - override suspend fun entersBackground() = coroutineScope { - lockJob = launch { - if (featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock)) { - //delay(GRACE_PERIOD_IN_MILLIS) - _lockScreenState.value = LockScreenState.Locked - } - } - } -} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockEvents.kt 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 e189a2ab39..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 @@ -17,35 +17,39 @@ 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.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 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 import javax.inject.Inject class PinUnlockPresenter @Inject constructor( - private val pinStateService: LockScreenStateService, + private val pinCodeManager: PinCodeManager, + private val matrixClient: MatrixClient, private val coroutineScope: CoroutineScope, ) : Presenter { @Composable override fun present(): PinUnlockState { - var pinEntry by remember { - //TODO fetch size from db - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + val pinEntryState = remember { + mutableStateOf>(Async.Uninitialized) } - var remainingAttempts by rememberSaveable { - //TODO fetch from db - mutableIntStateOf(3) + val pinEntry by pinEntryState + var remainingAttempts by remember { + mutableStateOf>(Async.Uninitialized) } var showWrongPinTitle by rememberSaveable { mutableStateOf(false) @@ -53,18 +57,48 @@ class PinUnlockPresenter @Inject constructor( var showSignOutPrompt by rememberSaveable { mutableStateOf(false) } + val signOutAction = remember { + 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) { + pinEntryState.value = 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() } - } + pinEntryState.value = pinEntry.process(event.pinKeypadModel) } PinUnlockEvents.OnForgetPin -> showSignOutPrompt = true PinUnlockEvents.ClearSignOutPrompt -> showSignOutPrompt = false + PinUnlockEvents.SignOut -> { + if (showSignOutPrompt) { + showSignOutPrompt = false + coroutineScope.signOut(signOutAction) + } + } + PinUnlockEvents.OnUseBiometric -> { + //TODO + } } } return PinUnlockState( @@ -72,15 +106,43 @@ class PinUnlockPresenter @Inject constructor( showWrongPinTitle = showWrongPinTitle, remainingAttempts = remainingAttempts, showSignOutPrompt = showSignOutPrompt, + signOutAction = signOutAction.value, eventSink = ::handleEvents ) } - private fun PinEntry.process(pinKeypadModel: PinKeypadModel): PinEntry { - return when (pinKeypadModel) { - PinKeypadModel.Back -> deleteLast() - is PinKeypadModel.Number -> addDigit(pinKeypadModel.number) - PinKeypadModel.Empty -> this + 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) + } } 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..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 @@ -17,13 +17,18 @@ 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 pinEntry: Async, val showWrongPinTitle: Boolean, - val remainingAttempts: Int, + val remainingAttempts: Async, val showSignOutPrompt: Boolean, + val signOutAction: Async, val eventSink: (PinUnlockEvents) -> Unit ) { - val isSignOutPromptCancellable = remainingAttempts > 0 + val isSignOutPromptCancellable = when (remainingAttempts) { + is Async.Success -> remainingAttempts.data > 0 + else -> true + } } 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..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 @@ -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 @@ -27,6 +28,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { aPinUnlockState(showWrongPinTitle = true), aPinUnlockState(showSignOutPrompt = true), aPinUnlockState(showSignOutPrompt = true, remainingAttempts = 0), + aPinUnlockState(signOutAction = Async.Loading()), ) } @@ -35,10 +37,12 @@ fun aPinUnlockState( remainingAttempts: Int = 3, showWrongPinTitle: Boolean = false, showSignOutPrompt: Boolean = false, + signOutAction: Async = Async.Uninitialized, ) = PinUnlockState( - pinEntry = pinEntry, + pinEntry = Async.Success(pinEntry), showWrongPinTitle = showWrongPinTitle, - remainingAttempts = remainingAttempts, + 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 5769f42b35..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 @@ -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, + ) } } @@ -226,10 +253,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 { + pluralStringResource(id = R.plurals.screen_app_lock_subtitle, count = remainingAttempts, remainingAttempts) + } } else { - stringResource(id = R.string.screen_app_lock_subtitle) + "" } val subtitleColor = if (state.showWrongPinTitle) { MaterialTheme.colorScheme.error @@ -244,17 +276,21 @@ 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, 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-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/main/res/values/localazy.xml b/features/lockscreen/impl/src/main/res/values/localazy.xml index 6b12eac427..bd6d522d57 100644 --- a/features/lockscreen/impl/src/main/res/values/localazy.xml +++ b/features/lockscreen/impl/src/main/res/values/localazy.xml @@ -1,15 +1,24 @@ + + "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" + "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" @@ -21,5 +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…" 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/fixtures/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt new file mode 100644 index 0000000000..bf9ebdf541 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt @@ -0,0 +1,33 @@ +/* + * 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.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 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/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") + } +} 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 } 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 ff797b52f4..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 @@ -20,12 +20,19 @@ 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.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 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 @@ -38,8 +45,13 @@ class SetupPinPresenterTest { @Test fun `present - complete flow`() = runTest { - - val presenter = createSetupPinPresenter() + val pinCodeCreated = CompletableDeferred() + val callback = object : DefaultPinCodeManagerCallback() { + override fun onPinCodeCreated() { + pinCodeCreated.complete(Unit) + } + } + val presenter = createSetupPinPresenter(callback) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -67,7 +79,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() @@ -86,7 +100,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,10 +112,23 @@ 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, + lockScreenConfig: LockScreenConfig = aLockScreenConfig( + pinBlacklist = setOf(blacklistedPin) + ), + ): SetupPinPresenter { + val pinCodeManager = aPinCodeManager() + pinCodeManager.addCallback(callback) + 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 02919edce0..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,13 +20,17 @@ 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.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.state.DefaultLockScreenStateService import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.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 +41,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 : DefaultPinCodeManagerCallback() { + 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 +70,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 : DefaultPinCodeManagerCallback() { + 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 +128,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 = DefaultPinCodeManagerCallback(), + ): PinUnlockPresenter { + val pinCodeManager = aPinCodeManager().apply { + addCallback(callback) + createPinCode(completePin) } - val lockScreenStateService = DefaultLockScreenStateService(featureFlagService) return PinUnlockPresenter( - lockScreenStateService, + pinCodeManager, + FakeMatrixClient(), scope, ) } 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..012c8e9a5c --- /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 + } +} 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/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-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 bb285968c6..233a9ecbf4 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" @@ -29,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" @@ -39,4 +39,5 @@ "Show less" "Hold to record" "Failed processing media to upload, please try again." + "Mentions and Keywords only" 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 0fcf04d1df..2b46fa0746 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 @@ -51,6 +52,7 @@ import kotlinx.parcelize.Parcelize class PreferencesFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val lockScreenEntryPoint: LockScreenEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(), @@ -82,6 +84,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object NotificationSettings : NavTarget + @Parcelize + data object LockScreenSettings : NavTarget + @Parcelize data class EditDefaultNotificationSetting(val isOneToOne: Boolean) : NavTarget @@ -117,6 +122,10 @@ class PreferencesFlowNode @AssistedInject constructor( backstack.push(NavTarget.NotificationSettings) } + override fun onOpenLockScreenSettings() { + backstack.push(NavTarget.LockScreenSettings) + } + override fun onOpenAdvancedSettings() { backstack.push(NavTarget.AdvancedSettings) } @@ -168,6 +177,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 = {}, ) } 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/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/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..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) + 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 85f499e404..5a5815c602 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -35,6 +35,7 @@ "Edit" "Enable" "End poll" + "Enter PIN" "Forgot password?" "Forward" "Invite" @@ -52,7 +53,7 @@ "No" "Not now" "OK" - "Open settings" + "Settings" "Open with" "Quick reply" "Quote" @@ -146,6 +147,7 @@ "Server URL" "Settings" "Shared location" + "Signing out" "Starting chat…" "Sticker" "Success" @@ -168,8 +170,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" @@ -269,6 +274,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/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 0fbfa13a57..33ba672bcd 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")) implementation(project(":libraries:voicerecorder:impl")) } 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..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:de1d44f62edd3f2d30421e49ad435971b165711b8bfe6d4a9475c5fb2f9f83ed -size 8506 +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 a4c7739624..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:d58d2d25d8f2c07c976bcda0eea7ec101f993ad1ef733fae4c713a67650e33e2 -size 8498 +oid sha256:db3f1674750094cef7b8e81a462ee4d2d180a2978dfa682f9db74bac5ad1a40d +size 11939 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 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" ] } ]