From c8b545887843333fdb120bbdaeae2c7c89d99020 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 9 Apr 2024 17:28:12 +0200 Subject: [PATCH] Add `SessionData.needsVerification` field (#2672) * Add `SessionData.needsVerification`: - Allows us to add a skip button for debug builds. - We can have the verification state almost instantly. - It doesn't depend on network availability to know the verification state and display the UI. * Add DB migration. - Make the skip button in the verification flow skip the whole flow including the completed screen. - Save the session as verified in `RustEncryptionService.recover(recoveryKey)`. * Enforce session verification for existing users too. * Fix verification confirmed screen subtitle (typo in id, was using the wrong string) * Update screenshots --------- Co-authored-by: ElementBot --- .maestro/tests/account/verifySession.yaml | 4 + .../FtueSessionVerificationFlowNode.kt | 8 +- .../ftue/impl/state/DefaultFtueService.kt | 10 +- .../ftue/impl/DefaultFtueServiceTests.kt | 3 +- .../signedout/impl/SignedOutStateProvider.kt | 2 + .../impl/VerifySelfSessionPresenter.kt | 24 +++- .../impl/VerifySelfSessionState.kt | 2 + .../impl/VerifySelfSessionStateProvider.kt | 8 +- .../impl/VerifySelfSessionView.kt | 37 ++++- .../impl/VerifySelfSessionViewEvents.kt | 1 + .../impl/VerifySelfSessionPresenterTests.kt | 78 +++++++++-- .../impl/VerifySelfSessionViewTest.kt | 53 ++++++++ .../SessionVerificationService.kt | 16 +++ .../libraries/matrix/impl/RustMatrixClient.kt | 4 + .../auth/RustMatrixAuthenticationService.kt | 4 +- .../impl/encryption/RustEncryptionService.kt | 7 +- .../libraries/matrix/impl/mapper/Session.kt | 2 + .../RustSessionVerificationService.kt | 128 ++++++++++-------- .../FakeSessionVerificationService.kt | 15 +- .../sessionstorage/api/SessionData.kt | 16 +++ .../sessionstorage/impl/SessionDataMapper.kt | 2 + .../impl/src/main/sqldelight/databases/6.db | Bin 0 -> 12288 bytes .../libraries/matrix/session/SessionData.sq | 4 +- .../impl/src/main/sqldelight/migrations/5.sqm | 4 + .../impl/DatabaseSessionStoreTests.kt | 6 + .../libraries/sessionstorage/impl/Fixtures.kt | 1 + ...ionView-Day-0_1_null_0,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_1,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_2,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_3,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_4,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_5,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_6,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_7,NEXUS_5,1.0,en].png | 4 +- ...ionView-Day-0_1_null_8,NEXUS_5,1.0,en].png | 3 + ...nView-Night-0_2_null_0,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_2,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_3,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_4,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_5,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_6,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_7,NEXUS_5,1.0,en].png | 4 +- ...nView-Night-0_2_null_8,NEXUS_5,1.0,en].png | 3 + 44 files changed, 386 insertions(+), 123 deletions(-) create mode 100644 libraries/session-storage/impl/src/main/sqldelight/databases/6.db create mode 100644 libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png diff --git a/.maestro/tests/account/verifySession.yaml b/.maestro/tests/account/verifySession.yaml index 5449f4f7da..73090f42da 100644 --- a/.maestro/tests/account/verifySession.yaml +++ b/.maestro/tests/account/verifySession.yaml @@ -7,3 +7,7 @@ appId: ${MAESTRO_APP_ID} - inputText: ${MAESTRO_RECOVERY_KEY} - hideKeyboard - tapOn: "Confirm" +- extendedWaitUntil: + visible: "Device verified" + timeout: 10000 +- tapOn: "Continue" diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt index 53e7b32995..a0f71eef97 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/FtueSessionVerificationFlowNode.kt @@ -19,11 +19,13 @@ package io.element.android.features.ftue.impl.sessionverification import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope 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 com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -33,6 +35,7 @@ import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.di.SessionScope +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -83,7 +86,10 @@ class FtueSessionVerificationFlowNode @AssistedInject constructor( .params(SecureBackupEntryPoint.Params(SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) .callback(object : SecureBackupEntryPoint.Callback { override fun onDone() { - callback.onDone() + lifecycleScope.launch { + // Move to the completed state view in the verification flow + backstack.newRoot(NavTarget.Root) + } } }) .build() diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt index 384d518452..87b7c3a7ee 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueService.kt @@ -25,7 +25,6 @@ import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.permissions.api.PermissionStateProvider import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider @@ -56,7 +55,7 @@ class DefaultFtueService @Inject constructor( } init { - sessionVerificationService.sessionVerifiedStatus + sessionVerificationService.needsVerificationFlow .onEach { updateState() } .launchIn(coroutineScope) @@ -99,12 +98,8 @@ class DefaultFtueService @Inject constructor( ).any { it() } } - private fun isSessionVerificationServiceReady(): Boolean { - return sessionVerificationService.sessionVerifiedStatus.value != SessionVerifiedStatus.Unknown - } - private fun isSessionNotVerified(): Boolean { - return sessionVerificationService.sessionVerifiedStatus.value == SessionVerifiedStatus.NotVerified + return sessionVerificationService.needsVerificationFlow.value } private fun needsAnalyticsOptIn(): Boolean { @@ -132,7 +127,6 @@ class DefaultFtueService @Inject constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun updateState() { state.value = when { - !isSessionVerificationServiceReady() -> FtueState.Unknown isAnyStepIncomplete() -> FtueState.Incomplete else -> FtueState.Complete } diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt index f3f21836b9..d381d945a9 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTests.kt @@ -90,6 +90,7 @@ class DefaultFtueServiceTests { fun `traverse flow`() = runTest { val sessionVerificationService = FakeSessionVerificationService().apply { givenVerifiedStatus(SessionVerifiedStatus.NotVerified) + givenNeedsVerification(true) } val analyticsService = FakeAnalyticsService() val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false) @@ -107,7 +108,7 @@ class DefaultFtueServiceTests { // Session verification steps.add(state.getNextStep(steps.lastOrNull())) - sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified) + sessionVerificationService.givenNeedsVerification(false) // Notifications opt in steps.add(state.getNextStep(steps.lastOrNull())) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt index df3549b0f8..9827d8d720 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt @@ -38,6 +38,7 @@ fun aSignedOutState() = SignedOutState( fun aSessionData( sessionId: SessionId = SessionId("@alice:server.org"), isTokenValid: Boolean = false, + needsVerification: Boolean = false, ): SessionData { return SessionData( userId = sessionId.value, @@ -51,5 +52,6 @@ fun aSessionData( isTokenValid = isTokenValid, loginType = LoginType.UNKNOWN, passphrase = null, + needsVerification = needsVerification, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt index a060469199..ecd263d47d 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt @@ -23,10 +23,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import com.freeletics.flowredux.compose.rememberStateAndDispatch import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -35,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState @@ -43,20 +48,28 @@ class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, private val stateMachine: VerifySelfSessionStateMachine, + private val buildMeta: BuildMeta, ) : Presenter { @Composable override fun present(): VerifySelfSessionState { + val coroutineScope = rememberCoroutineScope() LaunchedEffect(Unit) { // Force reset, just in case the service was left in a broken state sessionVerificationService.reset() } val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val stateAndDispatch = stateMachine.rememberStateAndDispatch() + var skipVerification by remember { mutableStateOf(false) } + val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState() val verificationFlowStep by remember { derivedStateOf { - stateAndDispatch.state.value.toVerificationStep( - canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE - ) + when { + skipVerification -> VerifySelfSessionState.VerificationStep.Skipped + needsVerification -> stateAndDispatch.state.value.toVerificationStep( + canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE + ) + else -> VerifySelfSessionState.VerificationStep.Completed + } } } // Start this after observing state machine @@ -72,10 +85,15 @@ class VerifySelfSessionPresenter @Inject constructor( VerifySelfSessionViewEvents.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset) + VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch { + sessionVerificationService.saveVerifiedState(true) + skipVerification = true + } } } return VerifySelfSessionState( verificationFlowStep = verificationFlowStep, + displaySkipButton = buildMeta.isDebuggable, eventSink = ::handleEvents, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt index fa3cb68adf..770915d9e3 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD @Immutable data class VerifySelfSessionState( val verificationFlowStep: VerificationStep, + val displaySkipButton: Boolean, val eventSink: (VerifySelfSessionViewEvents) -> Unit, ) { @Stable @@ -34,5 +35,6 @@ data class VerifySelfSessionState( data object Ready : VerificationStep data class Verifying(val data: SessionVerificationData, val state: AsyncData) : VerificationStep data object Completed : VerificationStep + data object Skipped : VerificationStep } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt index 59d42f11cd..c69d09acd7 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationEmoji open class VerifySelfSessionStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aVerifySelfSessionState(), + aVerifySelfSessionState(displaySkipButton = true), aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse ), @@ -46,6 +46,10 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider Unit = {}, ) = VerifySelfSessionState( verificationFlowStep = verificationFlowStep, + displaySkipButton = displaySkipButton, eventSink = eventSink, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt index 7a1ad12b09..8709411952 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt @@ -28,8 +28,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -51,11 +55,13 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep +@OptIn(ExperimentalMaterial3Api::class) @Composable fun VerifySelfSessionView( state: VerifySelfSessionState, @@ -66,6 +72,12 @@ fun VerifySelfSessionView( fun resetFlow() { state.eventSink(VerifySelfSessionViewEvents.Reset) } + val updatedOnFinished by rememberUpdatedState(newValue = onFinished) + LaunchedEffect(state.verificationFlowStep, updatedOnFinished) { + if (state.verificationFlowStep is FlowStep.Skipped) { + updatedOnFinished() + } + } BackHandler { when (state.verificationFlowStep) { is FlowStep.Canceled -> resetFlow() @@ -81,6 +93,19 @@ fun VerifySelfSessionView( val verificationFlowStep = state.verificationFlowStep HeaderFooterPage( modifier = modifier, + topBar = { + TopAppBar( + title = {}, + actions = { + if (state.displaySkipButton && state.verificationFlowStep != FlowStep.Completed) { + TextButton( + text = stringResource(CommonStrings.action_skip), + onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) } + ) + } + } + ) + }, header = { HeaderContent(verificationFlowStep = verificationFlowStep) }, @@ -104,6 +129,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { FlowStep.Canceled -> BigIcon.Style.AlertSolid FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction()) FlowStep.Completed -> BigIcon.Style.SuccessSolid + is FlowStep.Skipped -> return } val titleTextId = when (verificationFlowStep) { is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title @@ -114,20 +140,21 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title } + is FlowStep.Skipped -> return } val subtitleTextId = when (verificationFlowStep) { is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle - FlowStep.Completed -> R.string.screen_identity_confirmation_subtitle + FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle is FlowStep.Verifying -> when (verificationFlowStep.data) { is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle } + is FlowStep.Skipped -> return } PageTitle( - modifier = Modifier.padding(top = 60.dp), iconStyle = iconStyle, title = stringResource(id = titleTextId), subtitle = stringResource(id = subtitleTextId) @@ -137,9 +164,8 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { @Composable private fun Content(flowState: FlowStep) { Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { - when (flowState) { - is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit - is FlowStep.Verifying -> ContentVerifying(flowState) + if (flowState is FlowStep.Verifying) { + ContentVerifying(flowState) } } } @@ -264,6 +290,7 @@ private fun BottomMenu( onPositiveButtonClicked = onFinished, ) } + is FlowStep.Skipped -> return } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt index 2b86ca6f18..2c6b776f7b 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt @@ -23,4 +23,5 @@ sealed interface VerifySelfSessionViewEvents { data object DeclineVerification : VerifySelfSessionViewEvents data object Cancel : VerifySelfSessionViewEvents data object Reset : VerifySelfSessionViewEvents + data object SkipVerification : VerifySelfSessionViewEvents } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt index 025e782e8f..249dbdddab 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTests.kt @@ -23,15 +23,19 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -48,7 +52,21 @@ class VerifySelfSessionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + awaitItem().run { + assertThat(verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + assertThat(displaySkipButton).isTrue() + } + } + } + + @Test + fun `present - hides skip verification button on non-debuggable builds`() = runTest { + val buildMeta = aBuildMeta(isDebuggable = false) + val presenter = createVerifySelfSessionPresenter(buildMeta = buildMeta) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().displaySkipButton).isFalse() } } @@ -68,7 +86,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles requestVerification`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -79,7 +97,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Handles startSasVerification`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -113,7 +131,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - A failure when verifying cancels it`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -130,7 +148,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - A fail when requesting verification resets the state to the initial one`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -145,7 +163,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Canceling the flow once it's verifying cancels it`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -158,7 +176,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verifying, if we receive another challenge we ignore it`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -171,7 +189,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Restart after cancelation returns to requesting verification`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -188,7 +206,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - Go back after cancelation returns to initial state`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -208,7 +226,7 @@ class VerifySelfSessionPresenterTests { val emojis = listOf( VerificationEmoji(number = 30, emoji = "😀", description = "Smiley") ) - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -230,7 +248,7 @@ class VerifySelfSessionPresenterTests { @Test fun `present - When verification is declined, the flow is canceled`() = runTest { - val service = FakeSessionVerificationService() + val service = unverifiedSessionService() val presenter = createVerifySelfSessionPresenter(service) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -247,6 +265,33 @@ class VerifySelfSessionPresenterTests { } } + @Test + fun `present - Skip event skips the flow`() = runTest { + val service = unverifiedSessionService() + val presenter = createVerifySelfSessionPresenter(service) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = requestVerificationAndAwaitVerifyingState(service) + state.eventSink(VerifySelfSessionViewEvents.SkipVerification) + service.saveVerifiedStateResult.assertions().isCalledOnce().with(value(true)) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped) + } + } + + @Test + fun `present - When verification is not needed, the flow is completed`() = runTest { + val service = FakeSessionVerificationService().apply { + givenNeedsVerification(false) + } + val presenter = createVerifySelfSessionPresenter(service) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed) + } + } + private suspend fun ReceiveTurbine.requestVerificationAndAwaitVerifyingState( fakeService: FakeSessionVerificationService, sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), @@ -271,14 +316,23 @@ class VerifySelfSessionPresenterTests { return state } + private fun unverifiedSessionService(): FakeSessionVerificationService { + return FakeSessionVerificationService().apply { + givenVerifiedStatus(SessionVerifiedStatus.NotVerified) + givenNeedsVerification(true) + } + } + private fun createVerifySelfSessionPresenter( - service: SessionVerificationService = FakeSessionVerificationService(), + service: SessionVerificationService = unverifiedSessionService(), encryptionService: EncryptionService = FakeEncryptionService(), + buildMeta: BuildMeta = aBuildMeta(), ): VerifySelfSessionPresenter { return VerifySelfSessionPresenter( sessionVerificationService = service, encryptionService = encryptionService, stateMachine = VerifySelfSessionStateMachine(service, encryptionService), + buildMeta = buildMeta, ) } } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt index 1ca5bca32f..854ccac8af 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -22,6 +22,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnce @@ -126,6 +127,23 @@ class VerifySelfSessionViewTest { eventsRecorder.assertEmpty() } + @Test + fun `back key pressed - on Completed step does nothing`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed, + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + onFinished = EnsureNeverCalled(), + ) + } + rule.pressBackKey() + eventsRecorder.assertEmpty() + } + @Test fun `when flow is completed and the user clicks on the continue button, the expected callback is invoked`() { val eventsRecorder = EventsRecorder(expectEvents = false) @@ -202,4 +220,39 @@ class VerifySelfSessionViewTest { rule.clickOn(R.string.screen_session_verification_they_dont_match) eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) } + + @Test + fun `clicking on 'Skip' emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = true), + displaySkipButton = true, + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + onFinished = EnsureNeverCalled(), + ) + } + rule.clickOn(CommonStrings.action_skip) + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.SkipVerification) + } + + @Test + fun `on Skipped step - onFinished callback is called immediately`() { + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Skipped, + displaySkipButton = true, + eventSink = EnsureNeverCalledWithParam(), + ), + onEnterRecoveryKey = EnsureNeverCalled(), + onFinished = callback, + ) + } + } + } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt index 8d22fc174e..b82dd23188 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt @@ -21,6 +21,17 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow interface SessionVerificationService { + /** + * This flow stores the local verification status of the current session. + * + * We should ideally base the verified status in the Rust SDK info, but there are several issues with that approach: + * + * - The SDK takes a while to report this value, resulting in a delay of 1-2s in displaying the UI. + * - We need to add a 'Skip' option for testing purposes, which would not be possible if we relied only on the SDK. + * - The SDK sometimes doesn't report the verification state if there is no network connection when the app boots. + */ + val needsVerificationFlow: StateFlow + /** * State of the current verification flow ([VerificationFlowState.Initial] if not started). */ @@ -72,6 +83,11 @@ interface SessionVerificationService { * Returns the verification service state to the initial step. */ suspend fun reset() + + /** + * Saves the current session state as [verified]. + */ + suspend fun saveVerifiedState(verified: Boolean) } /** Verification status of the current session. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index cc1b4a0888..f67548afae 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -150,6 +150,7 @@ class RustMatrixClient( syncService = rustSyncService, sessionCoroutineScope = sessionCoroutineScope, dispatchers = dispatchers, + sessionStore = sessionStore, ) private val roomDirectoryService = RustRoomDirectoryService( @@ -177,6 +178,7 @@ class RustMatrixClient( isTokenValid = false, loginType = existingData.loginType, passphrase = existingData.passphrase, + needsVerification = existingData.needsVerification, ) sessionStore.updateData(newData) Timber.d("Removed session data with token: '...$anonymizedToken'.") @@ -204,6 +206,7 @@ class RustMatrixClient( isTokenValid = true, loginType = existingData.loginType, passphrase = existingData.passphrase, + needsVerification = existingData.needsVerification, ) sessionStore.updateData(newData) Timber.d("Saved new session data with token: '...$anonymizedToken'.") @@ -229,6 +232,7 @@ class RustMatrixClient( client = client, isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running }, sessionCoroutineScope = sessionCoroutineScope, + sessionStore = sessionStore, ) private val eventFilters = TimelineConfig.excludedEvents diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 29dd327ae2..ad34e30d18 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -148,6 +148,7 @@ class RustMatrixAuthenticationService @Inject constructor( isTokenValid = true, loginType = LoginType.PASSWORD, passphrase = pendingPassphrase, + needsVerification = true, ) } sessionStore.storeData(sessionData) @@ -195,7 +196,8 @@ class RustMatrixAuthenticationService @Inject constructor( it.session().toSessionData( isTokenValid = true, loginType = LoginType.OIDC, - passphrase = pendingPassphrase + passphrase = pendingPassphrase, + needsVerification = true, ) } pendingOidcAuthenticationData?.close() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt index f5a6390989..25881c2174 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustEncryptionService.kt @@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.impl.sync.RustSyncService +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.currentCoroutineContext @@ -48,10 +49,11 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException internal class RustEncryptionService( - client: Client, + private val client: Client, syncService: RustSyncService, sessionCoroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, + private val sessionStore: SessionStore, ) : EncryptionService { private val service: Encryption = client.encryption() @@ -186,6 +188,9 @@ internal class RustEncryptionService( override suspend fun recover(recoveryKey: String): Result = withContext(dispatchers.io) { runCatching { service.recover(recoveryKey) + val existingSession = sessionStore.getSession(client.userId()) + ?: error("Failed to save verification state. No session with id ${client.userId()}") + sessionStore.updateData(existingSession.copy(needsVerification = false)) }.mapFailure { it.mapRecoveryException() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt index aea838b705..3c1e3c40ec 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/mapper/Session.kt @@ -25,6 +25,7 @@ internal fun Session.toSessionData( isTokenValid: Boolean, loginType: LoginType, passphrase: String?, + needsVerification: Boolean, ) = SessionData( userId = userId, deviceId = deviceId, @@ -37,4 +38,5 @@ internal fun Session.toSessionData( isTokenValid = isTokenValid, loginType = loginType, passphrase = passphrase, + needsVerification = needsVerification, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index bb0821570b..3d2fe0cc40 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl.verification +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -23,108 +24,97 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu import io.element.android.libraries.matrix.api.verification.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.impl.util.cancelAndDestroy +import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Encryption import org.matrix.rustcomponents.sdk.RecoveryState import org.matrix.rustcomponents.sdk.RecoveryStateListener import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate -import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.VerificationState import org.matrix.rustcomponents.sdk.VerificationStateListener import org.matrix.rustcomponents.sdk.use import timber.log.Timber +import kotlin.time.Duration.Companion.seconds import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData class RustSessionVerificationService( - client: Client, + private val client: Client, isSyncServiceReady: Flow, - sessionCoroutineScope: CoroutineScope, + private val sessionCoroutineScope: CoroutineScope, + private val sessionStore: SessionStore, ) : SessionVerificationService, SessionVerificationControllerDelegate { - private var verificationStateListenerTaskHandle: TaskHandle? = null - private var recoveryStateListenerTaskHandle: TaskHandle? = null private val encryptionService: Encryption = client.encryption() private lateinit var verificationController: SessionVerificationController + // Listen for changes in verification status and update accordingly + private val verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener { + override fun onUpdate(status: VerificationState) { + Timber.d("New verification state: $status") + updateVerificationStatus(status) + } + }) + + // In case we enter the recovery key instead we check changes in the recovery state, since the listener above won't be triggered + private val recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RecoveryState) { + Timber.d("New recovery state: $status") + // We could check the `RecoveryState`, but it's easier to just use the verification state directly + updateVerificationStatus(encryptionService.verificationState()) + } + }) + + override val needsVerificationFlow: StateFlow = sessionStore.sessionsFlow() + .map { sessions -> sessions.firstOrNull { it.userId == client.userId() }?.needsVerification.orFalse() } + .distinctUntilChanged() + .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) + private val _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial) override val verificationFlowState = _verificationFlowState.asStateFlow() private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown) override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus.asStateFlow() - override val isReady = MutableStateFlow(false) + override val isReady = isSyncServiceReady.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) override val canVerifySessionFlow = combine(sessionVerifiedStatus, isReady) { verificationStatus, isReady -> isReady && verificationStatus == SessionVerifiedStatus.NotVerified } init { - isSyncServiceReady - .onEach { syncServiceReady -> - if (syncServiceReady) { - isReady.value = true - runCatching { - // If the controller was failed to initialize before, we try to get it again - if (!this::verificationController.isInitialized) { - verificationController = client.getSessionVerificationController() - } - } - .onFailure { - isReady.value = false - Timber.e(it, "Failed to get verification controller. Trying again in next sync.") - } - } else { - isReady.value = false - } - } - .launchIn(sessionCoroutineScope) - isReady.onEach { isReady -> if (isReady) { Timber.d("Starting verification service") - // Setup delegate - verificationController.setDelegate(this) - // Immediate status update updateVerificationStatus(encryptionService.verificationState()) - - // Listen for changes in verification status and update accordingly - verificationStateListenerTaskHandle?.cancelAndDestroy() - verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener { - override fun onUpdate(status: VerificationState) { - Timber.d("New verification state: $status") - updateVerificationStatus(status) - } - }) - - // In case we enter the recovery key instead we check changes in the recovery state, since the listener above won't be triggered - recoveryStateListenerTaskHandle?.cancelAndDestroy() - recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener { - override fun onUpdate(status: RecoveryState) { - Timber.d("New recovery state: $status") - // We could check the `RecoveryState`, but it's easier to just use the verification state directly - updateVerificationStatus(encryptionService.verificationState()) - } - }) } else { Timber.d("Stopping verification service") - if (this::verificationController.isInitialized) { - verificationController.setDelegate(null) - } } } .launchIn(sessionCoroutineScope) } override suspend fun requestVerification() = tryOrFail { + if (!this::verificationController.isInitialized) { + verificationController = client.getSessionVerificationController() + verificationController.setDelegate(this) + } verificationController.requestVerification() } @@ -164,9 +154,26 @@ class RustSessionVerificationService( } override fun didFinish() { - _verificationFlowState.value = VerificationFlowState.Finished - // Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it always returns false - updateVerificationStatus(VerificationState.VERIFIED) + sessionCoroutineScope.launch { + // Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it returns false if run immediately + // It also sometimes unexpectedly fails to report the session as verified, so we have to handle that possibility and fail if needed + runCatching { + withTimeout(30.seconds) { + while (!verificationController.isVerified()) { + delay(100) + } + } + } + .onSuccess { + saveVerifiedState(true) + updateVerificationStatus(VerificationState.VERIFIED) + _verificationFlowState.value = VerificationFlowState.Finished + } + .onFailure { + Timber.e(it, "Verification finished, but the Rust SDK still reports the session as unverified.") + didFail() + } + } } override fun didReceiveVerificationData(data: RustSessionVerificationData) { @@ -188,12 +195,21 @@ class RustSessionVerificationService( _verificationFlowState.value = VerificationFlowState.Initial } + override suspend fun saveVerifiedState(verified: Boolean) = tryOrFail { + val existingSession = sessionStore.getSession(client.userId()) + ?: error("Failed to save verification state. No session with id ${client.userId()}") + sessionStore.updateData(existingSession.copy(needsVerification = !verified)) + // Wait until the new state is saved + needsVerificationFlow.first { needsVerification -> !needsVerification } + } + fun destroy() { Timber.d("Destroying RustSessionVerificationService") - recoveryStateListenerTaskHandle?.cancelAndDestroy() + verificationStateListenerTaskHandle.cancelAndDestroy() + recoveryStateListenerTaskHandle.cancelAndDestroy() if (this::verificationController.isInitialized) { verificationController.setDelegate(null) - (verificationController as? SessionVerificationController)?.destroy() + verificationController.destroy() } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt index 64c7c8ab97..7823910263 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt @@ -20,17 +20,22 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.VerificationFlowState +import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder +import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeSessionVerificationService : SessionVerificationService { +class FakeSessionVerificationService( + var saveVerifiedStateResult: LambdaOneParamRecorder = lambdaRecorder {} +) : SessionVerificationService { private val _isReady = MutableStateFlow(false) private val _sessionVerifiedStatus = MutableStateFlow(SessionVerifiedStatus.Unknown) private var _verificationFlowState = MutableStateFlow(VerificationFlowState.Initial) private var _canVerifySessionFlow = MutableStateFlow(true) var shouldFail = false + override val needsVerificationFlow: MutableStateFlow = MutableStateFlow(false) override val verificationFlowState: StateFlow = _verificationFlowState override val sessionVerifiedStatus: StateFlow = _sessionVerifiedStatus override val canVerifySessionFlow: Flow = _canVerifySessionFlow @@ -89,7 +94,15 @@ class FakeSessionVerificationService : SessionVerificationService { _isReady.value = value } + fun givenNeedsVerification(value: Boolean) { + needsVerificationFlow.value = value + } + override suspend fun reset() { _verificationFlowState.value = VerificationFlowState.Initial } + + override suspend fun saveVerifiedState(verified: Boolean) { + saveVerifiedStateResult(verified) + } } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index 7189442716..dffc1d1bbb 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -18,16 +18,32 @@ package io.element.android.libraries.sessionstorage.api import java.util.Date +/** + * Data class representing the session data to store locally. + */ data class SessionData( + /** The user ID of the logged in user. */ val userId: String, + /** The device ID of the session. */ val deviceId: String, + /** The current access token of the session. */ val accessToken: String, + /** The optional current refresh token of the session. */ val refreshToken: String?, + /** The homeserver URL of the session. */ val homeserverUrl: String, + /** The Open ID Connect info for this session, if any. */ val oidcData: String?, + /** The Sliding Sync Proxy URL for this session, if any. */ val slidingSyncProxy: String?, + /** The timestamp of the last login. May be `null` in very old sessions. */ val loginTimestamp: Date?, + /** Whether the [accessToken] is valid or not. */ val isTokenValid: Boolean, + /** The login type used to authenticate the session. */ val loginType: LoginType, + /** The optional passphrase used to encrypt data in the SDK local store. */ val passphrase: String?, + /** Whether the session needs verification. */ + val needsVerification: Boolean, ) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index 3824def48c..6b79e867d0 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -34,6 +34,7 @@ internal fun SessionData.toDbModel(): DbSessionData { isTokenValid = if (isTokenValid) 1L else 0L, loginType = loginType.name, passphrase = passphrase, + needsVerification = if (needsVerification) 1L else 0L, ) } @@ -50,5 +51,6 @@ internal fun DbSessionData.toApiModel(): SessionData { isTokenValid = isTokenValid == 1L, loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name), passphrase = passphrase, + needsVerification = needsVerification == 1L, ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/databases/6.db b/libraries/session-storage/impl/src/main/sqldelight/databases/6.db new file mode 100644 index 0000000000000000000000000000000000000000..8bf785b442728b0ce8c65ba21f79985209ae3320 GIT binary patch literal 12288 zcmeI#&u`N(6bEppAV8I>?Y0xrlX?Nc5Mzup9m}SQFh-kB>{OZfb+t&7VrSKQ=YY8G zFXF%Ai0x3V{8H}YYsqQd_nv=z+8#W7m*|RVQkt2pXjg2CC=xG;gbMEv1Rwwb2tWV=5P$##AOL~?NZ|9M=Nqtp(^on_RoFZ2F4Q_ zdL5rV4q1v9I^*B->X^(jw{lv3agL7VLQE!*B?*I8M~zyY(^&kTb<3d(#833 zS!5@syj+Hyx^{b;q*AO%7vv`Jr>19NpLuz%i&VR*D><7}|2U10;=#YGc0*OERp!|m zmUteLb$!%R>Gro;;9Bk=KJ1N>ly>Sh>-{X}+%WZCn0hX)ollLlP3bQ}dcmyhDH}b} znXEj*&-4Ddou}=Gd#yWL{ca?5q4?5%%H3^#EHA1ujK^y~jPcIez~Afd`TtbMKQ{dX z4FV8=00bZa0SG_<0uX=z1Rwx`n<~)u2gCjUrhdIR7X%;x0SG_<0uX=z1Rwwb2tXhR F`~X5r!8HH? literal 0 HcmV?d00001 diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index c33b4d7c7e..56b036ec96 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -23,7 +23,9 @@ CREATE TABLE SessionData ( isTokenValid INTEGER NOT NULL DEFAULT 1, loginType TEXT, -- added in version 5 - passphrase TEXT + passphrase TEXT, + -- added in version 6 + needsVerification INTEGER NOT NULL DEFAULT 0 ); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm new file mode 100644 index 0000000000..22797f1049 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/5.sqm @@ -0,0 +1,4 @@ +-- Migrate DB from version 5 +-- For users migrating previously logged in sessions, we force them to verify them too + +ALTER TABLE SessionData ADD COLUMN needsVerification INTEGER NOT NULL DEFAULT 1; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index 46e90f6d52..df35ec5944 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -144,6 +144,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphrase", + needsVerification = 1L, ) val secondSessionData = SessionData( userId = "userId", @@ -157,6 +158,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphraseAltered", + needsVerification = 0L, ) assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId) assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp) @@ -177,6 +179,7 @@ class DatabaseSessionStoreTests { assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData) assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase) + assertThat(alteredSession.needsVerification).isEqualTo(secondSessionData.needsVerification) } @Test @@ -193,6 +196,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphrase", + needsVerification = 1L, ) val secondSessionData = SessionData( userId = "userIdUnknown", @@ -206,6 +210,7 @@ class DatabaseSessionStoreTests { isTokenValid = 1, loginType = null, passphrase = "aPassphraseAltered", + needsVerification = 0L, ) assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId) @@ -224,5 +229,6 @@ class DatabaseSessionStoreTests { assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp) assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData) assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase) + assertThat(notAlteredSession.needsVerification).isEqualTo(firstSessionData.needsVerification) } } diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt index 341e5e0e92..1397e260b9 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/Fixtures.kt @@ -31,4 +31,5 @@ internal fun aSessionData() = SessionData( isTokenValid = 1, loginType = LoginType.UNKNOWN.name, passphrase = null, + needsVerification = 0L, ) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png index 87165509bc..5fe4883b6f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef711f98b345d4913e930a4ef2794d1d4ae2f19e29f842548664713dbc2f82c -size 27670 +oid sha256:3fedeb54cfb4b08b3a75dc528b518966938ac9504471741cd9c0bcb90bab08f1 +size 28636 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png index cdd4902766..d22bcffed1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97b2c76523b5475a45d7023960356feee4748ef3be6fa6a33b0f727b327dc667 -size 25541 +oid sha256:761bb174a847aa9fd33f3bbba08473d36fb8319259dbef89d6f907581600042a +size 25323 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png index dddcf52c2f..431dc18371 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f6269ea5f1be9794e768671ba27d44932ce8d7a2d2d599ad2a9837e6346d7ba -size 50761 +oid sha256:c5cd4953883ddc280d12986db679383d69f9fd491e25e6719a309d0e891d6f6b +size 50793 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png index ff7200bbe7..d1d24b44f5 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1ba54ee6e97535ca0a350f687ea64e826c1c590e67fddf95ff5e6f85c9ca0ea -size 52342 +oid sha256:50d3fb9d04f37ef7b78c6f2736d627da26e137317720e9b1de8d773e3ca12dd6 +size 52382 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png index eeb7a029bb..5a77351c06 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb6e3df51c248b69357ec7dd3dd8c5ba8bb8c579c8f08e5279039c78d50c541e -size 31024 +oid sha256:909c750c58d01cff00802db2218c52fc7c13f3733996b6c4e5d204daf464ae1e +size 30718 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png index 771889d3c2..45b14790fc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa8ed00e0184ac4636fbf26b42dd10893e1bc9aa64b6107922d7ad97e7304d80 -size 21927 +oid sha256:2994892e230bf79fd9dc6eed9c21854c4ee441fef1b87e65207504de125c0e74 +size 21847 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png index e37ce30fa7..0559f67af1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae000f0f2e0f81cf75588d800f3b623f875db1dee7d5459abebfb1cbb67d5da1 -size 34937 +oid sha256:0dc7d7114d4365aa6f6a5fd645fd448e6088b2fd618efa49543eb2dbe0a1b394 +size 34779 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png index 87165509bc..042131d574 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef711f98b345d4913e930a4ef2794d1d4ae2f19e29f842548664713dbc2f82c -size 27670 +oid sha256:92d98ccd5cf7f64cdeb531f0bc4f48589688661a6338de0628c2815b12e59d86 +size 27456 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9bef6f55d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Day-0_1_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37e571dda745d7d71df2d421651fbed56a8e698583b6b2684633574ffc8c56a9 +size 28880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png index e2a4d4b4b3..b3cddf5860 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8cb1d19005b68d1f9f09240f58937bdf30efd00bf87758693791bda6627191f -size 25745 +oid sha256:abecf71fc305a7fd44f791ca72072f34601a49dcd1eafd4fbfa34fa58cf3db2a +size 26800 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png index 74ea9fb0bc..2d08d6b799 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1141421ad8fc3ebc2e321a45604dd1d6dc4494a9ae7b860e7853e4a0cd610d5f -size 23697 +oid sha256:55decf810e2d313554710cab91decd8dbd11ee2d18eeeec760f327b0bcc24063 +size 23507 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png index a3bd57d7a0..f3c45f52d3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a1b73e341ce8c0932f1253b9ee159a219e1b64abef54f14b286ac551b2542e4 -size 48479 +oid sha256:4d5458b28ad6ba716752234a8439b72fed9133c1e4b2b3fa3df2fe87f03df6ae +size 48369 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png index 1ccd2010c9..cd697f8dbf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f06386b912ef02aa34cd1806588c065e76ab1373892cd62ded705537dd593a81 -size 50027 +oid sha256:c7662be2a6450384daa9e3d92c15891e53043b3181c026f4051443fb62334c96 +size 49923 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png index 62977b86c3..472438841e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:118b4d64a5977842c3d65d27bcf545cef8b5bf62f8417ab66934553fd3985b67 -size 29534 +oid sha256:de24ec97169e51b1d820e2eccdd283e7cc666635159384d8a6e62704b168ed6f +size 29404 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png index e00771d913..e38d40d6fb 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35409dad83ef81e0aaa74a36b350e028d07a5d90cdcc75c29f9156fbdeb2e3c1 -size 20771 +oid sha256:23009d617608b10fe8a4ad6363c0a4feebb49ff160c0bd4c308eda1b2fc258a2 +size 20608 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png index 507ff9cd8e..59ee74b726 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5da97854c7dd24f5f890ff1bcce8e8061892f2a66b919caf254c85ecf1831c38 -size 32717 +oid sha256:971e3b14de6ed22d1c15687fdfe3bd2a56b5665ac250ae868ed7afe427778d1f +size 32510 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png index e2a4d4b4b3..1abc88027b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8cb1d19005b68d1f9f09240f58937bdf30efd00bf87758693791bda6627191f -size 25745 +oid sha256:5ab2e5a17ffb9ae5fa5b62a9562920281af9e2d9d2c51b80fae4c973497d118a +size 25545 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2b9295e65e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.verifysession.impl_VerifySelfSessionView_null_VerifySelfSessionView-Night-0_2_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c39cda078df3c3ac3297c86d4b3da332f2793b53d62a49ed7b3f6349b9059404 +size 27770