From 2d5272785167f335a9d9667e1dd8594c5f0fce81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:03:29 +0000 Subject: [PATCH 01/27] Update dependency io.sentry:sentry-android to v7.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7fc352d3bf..9dcb238f1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -173,7 +173,7 @@ kotlinpoet = "com.squareup:kotlinpoet:1.16.0" # Analytics posthog = "com.posthog:posthog-android:3.1.8" -sentry = "io.sentry:sentry-android:7.3.0" +sentry = "io.sentry:sentry-android:7.4.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.11.0" # Emojibase From 7ec876bc8ac61358fac4ec2146a7aec701e16820 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Feb 2024 17:23:35 +0100 Subject: [PATCH 02/27] Rename `isLastSession` to `isLastDevice`, to match SDK naming. --- .../features/logout/impl/LogoutPresenter.kt | 6 +++--- .../android/features/logout/impl/LogoutState.kt | 2 +- .../features/logout/impl/LogoutStateProvider.kt | 14 +++++++------- .../android/features/logout/impl/LogoutView.kt | 6 +++--- .../impl/direct/DefaultDirectLogoutPresenter.kt | 6 +++--- .../features/logout/impl/LogoutPresenterTest.kt | 6 +++--- .../android/features/logout/impl/LogoutViewTest.kt | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index ed735f6afe..f56a24cef5 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -66,9 +66,9 @@ class LogoutPresenter @Inject constructor( } .collectAsState(initial = BackupUploadState.Unknown) - var isLastSession by remember { mutableStateOf(false) } + var isLastDevice by remember { mutableStateOf(false) } LaunchedEffect(Unit) { - isLastSession = encryptionService.isLastDevice().getOrNull() ?: false + isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false } val backupState by encryptionService.backupStateStateFlow.collectAsState() @@ -100,7 +100,7 @@ class LogoutPresenter @Inject constructor( } return LogoutState( - isLastSession = isLastSession, + isLastDevice = isLastDevice, backupState = backupState, doesBackupExistOnServer = doesBackupExistOnServerAction.value.dataOrNull().orTrue(), recoveryState = recoveryState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt index 6da9df8e72..4b0121d052 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.RecoveryState data class LogoutState( - val isLastSession: Boolean, + val isLastDevice: Boolean, val backupState: BackupState, val doesBackupExistOnServer: Boolean, val recoveryState: RecoveryState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt index f1b11f61e9..dc563daa33 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt @@ -27,22 +27,22 @@ open class LogoutStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLogoutState(), - aLogoutState(isLastSession = true), - aLogoutState(isLastSession = false, backupUploadState = BackupUploadState.Uploading(66, 200)), - aLogoutState(isLastSession = true, backupUploadState = BackupUploadState.Done), + aLogoutState(isLastDevice = true), + aLogoutState(isLastDevice = false, backupUploadState = BackupUploadState.Uploading(66, 200)), + aLogoutState(isLastDevice = true, backupUploadState = BackupUploadState.Done), aLogoutState(logoutAction = AsyncAction.Confirming), aLogoutState(logoutAction = AsyncAction.Loading), aLogoutState(logoutAction = AsyncAction.Failure(Exception("Failed to logout"))), aLogoutState(backupUploadState = BackupUploadState.SteadyException(SteadyStateException.Connection("No network"))), // Last session no recovery - aLogoutState(isLastSession = true, recoveryState = RecoveryState.DISABLED), + aLogoutState(isLastDevice = true, recoveryState = RecoveryState.DISABLED), // Last session no backup - aLogoutState(isLastSession = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false), + aLogoutState(isLastDevice = true, backupState = BackupState.UNKNOWN, doesBackupExistOnServer = false), ) } fun aLogoutState( - isLastSession: Boolean = false, + isLastDevice: Boolean = false, backupState: BackupState = BackupState.ENABLED, doesBackupExistOnServer: Boolean = true, recoveryState: RecoveryState = RecoveryState.ENABLED, @@ -50,7 +50,7 @@ fun aLogoutState( logoutAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (LogoutEvents) -> Unit = {}, ) = LogoutState( - isLastSession = isLastSession, + isLastDevice = isLastDevice, backupState = backupState, doesBackupExistOnServer = doesBackupExistOnServer, recoveryState = recoveryState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index 90168c09dd..19160236ac 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -97,7 +97,7 @@ fun LogoutView( private fun title(state: LogoutState): String { return when { state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title) - state.isLastSession -> { + state.isLastDevice -> { if (state.recoveryState != RecoveryState.ENABLED) { stringResource(id = R.string.screen_signout_recovery_disabled_title) } else if (state.backupState == BackupState.UNKNOWN && state.doesBackupExistOnServer.not()) { @@ -116,7 +116,7 @@ private fun subtitle(state: LogoutState): String? { (state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection -> stringResource(id = R.string.screen_signout_key_backup_offline_subtitle) state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle) - state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) + state.isLastDevice -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) else -> null } } @@ -128,7 +128,7 @@ private fun ColumnScope.Buttons( onChangeRecoveryKeyClicked: () -> Unit, ) { val logoutAction = state.logoutAction - if (state.isLastSession) { + if (state.isLastDevice) { OutlinedButton( text = stringResource(id = CommonStrings.common_settings), modifier = Modifier.fillMaxWidth(), diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt index 8ee46b63ac..d9a6e7e94c 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt @@ -70,9 +70,9 @@ class DefaultDirectLogoutPresenter @Inject constructor( } .collectAsState(initial = BackupUploadState.Unknown) - var isLastSession by remember { mutableStateOf(false) } + var isLastDevice by remember { mutableStateOf(false) } LaunchedEffect(Unit) { - isLastSession = encryptionService.isLastDevice().getOrNull() ?: false + isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false } fun handleEvents(event: DirectLogoutEvents) { @@ -91,7 +91,7 @@ class DefaultDirectLogoutPresenter @Inject constructor( } return DirectLogoutState( - canDoDirectSignOut = !isLastSession && + canDoDirectSignOut = !isLastDevice && !backupUploadState.isBackingUp(), logoutAction = logoutAction.value, eventSink = ::handleEvents diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index 83b7799c3a..8fab9f3e01 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -50,7 +50,7 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - assertThat(initialState.isLastSession).isFalse() + assertThat(initialState.isLastDevice).isFalse() assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN) assertThat(initialState.doesBackupExistOnServer).isTrue() assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN) @@ -71,7 +71,7 @@ class LogoutPresenterTest { }.test { skipItems(3) val initialState = awaitItem() - assertThat(initialState.isLastSession).isTrue() + assertThat(initialState.isLastDevice).isTrue() assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -96,7 +96,7 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.isLastSession).isFalse() + assertThat(initialState.isLastDevice).isFalse() assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) skipItems(1) diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt index 697da3cad5..78cc65b117 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt @@ -153,7 +153,7 @@ class LogoutViewTest { rule.setContent { LogoutView( aLogoutState( - isLastSession = true, + isLastDevice = true, eventSink = eventsRecorder ), onChangeRecoveryKeyClicked = callback, From 4bc977d8dcc68fbb804bdcdceb2809bdd7e26a10 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 10:23:48 +0100 Subject: [PATCH 03/27] Introduce SecurityBannerState to replace Boolean set. Also get the sessionVerificationService from the matrixClient, instead of injecting it separately. --- .../roomlist/impl/RoomListPresenter.kt | 32 +++++++++--------- .../features/roomlist/impl/RoomListState.kt | 9 +++-- .../roomlist/impl/RoomListStateProvider.kt | 7 ++-- .../features/roomlist/impl/RoomListView.kt | 32 +++++++++--------- .../roomlist/impl/RoomListPresenterTests.kt | 33 +++++++------------ .../android/samples/minimal/RoomListScreen.kt | 1 - 6 files changed, 55 insertions(+), 59 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 3349eacc55..d5a09355cb 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -73,7 +73,6 @@ private const val EXTENDED_RANGE_SIZE = 40 class RoomListPresenter @Inject constructor( private val client: MatrixClient, - private val sessionVerificationService: SessionVerificationService, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val inviteStateDataSource: InviteStateDataSource, @@ -87,6 +86,7 @@ class RoomListPresenter @Inject constructor( private val analyticsService: AnalyticsService, ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() + private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService() @Composable override fun present(): RoomListState { @@ -108,22 +108,25 @@ class RoomListPresenter @Inject constructor( val isMigrating = migrationScreenPresenter.present().isMigrating - // Session verification status (unknown, not verified, verified) - val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) - var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) } - // We combine both values to only display the prompt if the session is not verified and it wasn't dismissed - val displayVerificationPrompt by remember { - derivedStateOf { canVerifySession && !verificationPromptDismissed } - } + var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } + val displayVerificationPrompt by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) .collectAsState(initial = null) - var recoveryKeyPromptDismissed by rememberSaveable { mutableStateOf(false) } val displayRecoveryKeyPrompt by remember { derivedStateOf { secureStorageFlag == true && - recoveryState == RecoveryState.INCOMPLETE && - !recoveryKeyPromptDismissed + recoveryState == RecoveryState.INCOMPLETE + } + } + val securityBannerState by remember { + derivedStateOf { + when { + securityBannerDismissed -> SecurityBannerState.None + displayVerificationPrompt -> SecurityBannerState.SessionVerification + displayRecoveryKeyPrompt -> SecurityBannerState.RecoveryKeyConfirmation + else -> SecurityBannerState.None + } } } @@ -135,8 +138,8 @@ class RoomListPresenter @Inject constructor( fun handleEvents(event: RoomListEvents) { when (event) { is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range) - RoomListEvents.DismissRequestVerificationPrompt -> verificationPromptDismissed = true - RoomListEvents.DismissRecoveryKeyPrompt -> recoveryKeyPromptDismissed = true + RoomListEvents.DismissRequestVerificationPrompt -> securityBannerDismissed = true + RoomListEvents.DismissRecoveryKeyPrompt -> securityBannerDismissed = true RoomListEvents.ToggleSearchResults -> searchState.eventSink(RoomListSearchEvents.ToggleSearchVisibility) is RoomListEvents.ShowContextMenu -> { coroutineScope.showContextMenu(event, contextMenu) @@ -157,8 +160,7 @@ class RoomListPresenter @Inject constructor( matrixUser = matrixUser.value, showAvatarIndicator = showAvatarIndicator, roomList = roomList, - displayVerificationPrompt = displayVerificationPrompt, - displayRecoveryKeyPrompt = displayRecoveryKeyPrompt, + securityBannerState = securityBannerState, snackbarMessage = snackbarMessage, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, invitesState = inviteStateDataSource.inviteState(), diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index b89cb2a25c..076f6bb10e 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -31,8 +31,7 @@ data class RoomListState( val matrixUser: MatrixUser?, val showAvatarIndicator: Boolean, val roomList: AsyncData>, - val displayVerificationPrompt: Boolean, - val displayRecoveryKeyPrompt: Boolean, + val securityBannerState: SecurityBannerState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val invitesState: InvitesState, @@ -62,3 +61,9 @@ enum class InvitesState { SeenInvites, NewInvites, } + +enum class SecurityBannerState { + None, + SessionVerification, + RecoveryKeyConfirmation, +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index ed1a948400..e35e785e70 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -37,14 +37,14 @@ open class RoomListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomListState(), - aRoomListState().copy(displayVerificationPrompt = true), + aRoomListState().copy(securityBannerState = SecurityBannerState.SessionVerification), aRoomListState().copy(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), aRoomListState().copy(hasNetworkConnection = false), aRoomListState().copy(invitesState = InvitesState.SeenInvites), aRoomListState().copy(invitesState = InvitesState.NewInvites), aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")), aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)), - aRoomListState().copy(displayRecoveryKeyPrompt = true), + aRoomListState().copy(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), aRoomListState().copy(matrixUser = null, displayMigrationStatus = true), @@ -58,8 +58,7 @@ internal fun aRoomListState() = RoomListState( roomList = AsyncData.Success(aRoomListRoomSummaryList()), hasNetworkConnection = true, snackbarMessage = null, - displayVerificationPrompt = false, - displayRecoveryKeyPrompt = false, + securityBannerState = SecurityBannerState.None, invitesState = InvitesState.NoInvites, contextMenu = RoomListState.ContextMenu.Hidden, leaveRoomState = aLeaveRoomState(), diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index ff956b0ec3..575ace8cf7 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -225,23 +225,25 @@ private fun RoomListContent( // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 contentPadding = PaddingValues(bottom = 80.dp) ) { - when { - state.displayEmptyState -> Unit - state.displayVerificationPrompt -> { - item { - RequestVerificationHeader( - onVerifyClicked = onVerifyClicked, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } - ) + if (state.displayEmptyState.not()) { + when (state.securityBannerState) { + SecurityBannerState.SessionVerification -> { + item { + RequestVerificationHeader( + onVerifyClicked = onVerifyClicked, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } + ) + } } - } - state.displayRecoveryKeyPrompt -> { - item { - ConfirmRecoveryKeyBanner( - onContinueClicked = onOpenSettings, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } - ) + SecurityBannerState.RecoveryKeyConfirmation -> { + item { + ConfirmRecoveryKeyBanner( + onContinueClicked = onOpenSettings, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + ) + } } + SecurityBannerState.None -> Unit } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 57afadcc64..c62d7a3adb 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -55,8 +55,6 @@ import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.timeline.ReceiptType -import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -114,13 +112,13 @@ class RoomListPresenterTests { fun `present - show avatar indicator`() = runTest { val scope = CoroutineScope(coroutineContext + SupervisorJob()) val encryptionService = FakeEncryptionService() + val sessionVerificationService = FakeSessionVerificationService() val matrixClient = FakeMatrixClient( encryptionService = encryptionService, + sessionVerificationService = sessionVerificationService, ) - val sessionVerificationService = FakeSessionVerificationService() val presenter = createRoomListPresenter( client = matrixClient, - sessionVerificationService = sessionVerificationService, coroutineScope = scope ) moleculeFlow(RecompositionMode.Immediate) { @@ -239,27 +237,17 @@ class RoomListPresenterTests { @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { - val roomListService = FakeRoomListService() - val matrixClient = FakeMatrixClient( - roomListService = roomListService, - ) val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( - client = matrixClient, - sessionVerificationService = FakeSessionVerificationService().apply { - givenIsReady(true) - givenVerifiedStatus(SessionVerifiedStatus.NotVerified) - }, coroutineScope = scope, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val eventSink = awaitItem().eventSink - assertThat(awaitItem().displayVerificationPrompt).isTrue() - + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.SessionVerification) eventSink(RoomListEvents.DismissRequestVerificationPrompt) - assertThat(awaitItem().displayVerificationPrompt).isFalse() + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() } } @@ -269,6 +257,9 @@ class RoomListPresenterTests { val encryptionService = FakeEncryptionService() val matrixClient = FakeMatrixClient( encryptionService = encryptionService, + sessionVerificationService = FakeSessionVerificationService().apply { + givenCanVerifySession(false) + }, ) val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( @@ -280,13 +271,13 @@ class RoomListPresenterTests { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.displayRecoveryKeyPrompt).isFalse() + assertThat(initialState.securityBannerState).isEqualTo(SecurityBannerState.None) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) val nextState = awaitItem() - assertThat(nextState.displayRecoveryKeyPrompt).isTrue() + assertThat(nextState.securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) nextState.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) val finalState = awaitItem() - assertThat(finalState.displayRecoveryKeyPrompt).isFalse() + assertThat(finalState.securityBannerState).isEqualTo(SecurityBannerState.None) scope.cancel() } } @@ -579,7 +570,6 @@ class RoomListPresenterTests { private fun TestScope.createRoomListPresenter( client: MatrixClient = FakeMatrixClient(), - sessionVerificationService: SessionVerificationService = FakeSessionVerificationService(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(), @@ -599,7 +589,6 @@ class RoomListPresenterTests { searchPresenter: Presenter = Presenter { aRoomListSearchState() }, ) = RoomListPresenter( client = client, - sessionVerificationService = sessionVerificationService, networkMonitor = networkMonitor, snackbarDispatcher = snackbarDispatcher, inviteStateDataSource = inviteStateDataSource, @@ -616,7 +605,7 @@ class RoomListPresenterTests { ), featureFlagService = featureFlagService, indicatorService = DefaultIndicatorService( - sessionVerificationService = sessionVerificationService, + sessionVerificationService = client.sessionVerificationService(), encryptionService = client.encryptionService(), featureFlagService = featureFlagService, ), diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 17e948a1a8..1d5b27f731 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -90,7 +90,6 @@ class RoomListScreen( ) private val presenter = RoomListPresenter( client = matrixClient, - sessionVerificationService = sessionVerificationService, networkMonitor = NetworkMonitorImpl(context, Singleton.appScope), snackbarDispatcher = SnackbarDispatcher(), inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), From 663c7ceefe36b35c76fa02d910fd95df97d00c9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 10:38:37 +0100 Subject: [PATCH 04/27] Sync strings. --- features/roomlist/impl/src/main/res/values/localazy.xml | 4 ++-- features/securebackup/impl/src/main/res/values/localazy.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index d9980835f4..edfa414381 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -1,7 +1,7 @@ - "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" + "Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup." + "Enter your recovery key" "This is a one time process, thanks for waiting." "Setting up your account." "Create a new conversation or room" diff --git a/features/securebackup/impl/src/main/res/values/localazy.xml b/features/securebackup/impl/src/main/res/values/localazy.xml index 613ff72719..f05725a075 100644 --- a/features/securebackup/impl/src/main/res/values/localazy.xml +++ b/features/securebackup/impl/src/main/res/values/localazy.xml @@ -5,7 +5,7 @@ "Backup ensures that you don\'t lose your message history. %1$s." "Backup" "Change recovery key" - "Confirm recovery key" + "Enter recovery key" "Your chat backup is currently out of sync." "Set up recovery" "Get access to your encrypted messages if you lose all your devices or are signed out of %1$s everywhere." @@ -27,7 +27,7 @@ "Enter the 48 character code." "Enter…" "Recovery key confirmed" - "Confirm your recovery key" + "Enter your recovery key" "Copied recovery key" "Generating…" "Save recovery key" From e959ca018673d1cf81ec37022af11612c898893f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 10:51:35 +0100 Subject: [PATCH 05/27] Do not show verification banner for the last device, show the enter recovery key banner. --- .../roomlist/impl/RoomListPresenter.kt | 23 +++++++++--------- .../roomlist/impl/RoomListPresenterTests.kt | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index d5a09355cb..1467595de0 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -109,22 +109,23 @@ class RoomListPresenter @Inject constructor( val isMigrating = migrationScreenPresenter.present().isMigrating var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } - val displayVerificationPrompt by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) - val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - val displayRecoveryKeyPrompt by remember { - derivedStateOf { - secureStorageFlag == true && - recoveryState == RecoveryState.INCOMPLETE - } + val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) + var isLastDevice by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false } + val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage).collectAsState(initial = null) val securityBannerState by remember { derivedStateOf { when { securityBannerDismissed -> SecurityBannerState.None - displayVerificationPrompt -> SecurityBannerState.SessionVerification - displayRecoveryKeyPrompt -> SecurityBannerState.RecoveryKeyConfirmation + canVerifySession -> if (isLastDevice) { + SecurityBannerState.RecoveryKeyConfirmation + } else { + SecurityBannerState.SessionVerification + } + secureStorageFlag == true && recoveryState == RecoveryState.INCOMPLETE -> SecurityBannerState.RecoveryKeyConfirmation else -> SecurityBannerState.None } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index c62d7a3adb..8b937db63d 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -235,6 +235,30 @@ class RoomListPresenterTests { } } + @Test + fun `present - handle RecoveryKeyConfirmation last session`() = runTest { + val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) + val presenter = createRoomListPresenter( + coroutineScope = scope, + client = FakeMatrixClient( + encryptionService = FakeEncryptionService().apply { + givenIsLastDevice(true) + } + ), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(2) + val eventSink = awaitItem().eventSink + // For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) + eventSink(RoomListEvents.DismissRequestVerificationPrompt) + assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.None) + scope.cancel() + } + } + @Test fun `present - handle DismissRequestVerificationPrompt`() = runTest { val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) From 42e990e472ab951a8f0b0cedb91afc2dc14df236 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 10:53:57 +0100 Subject: [PATCH 06/27] Do the opposite (was it a bug :thinking:) --- .../android/libraries/indicator/impl/DefaultIndicatorService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt index 80af72d6a9..265e3d2d6f 100644 --- a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt +++ b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt @@ -46,7 +46,7 @@ class DefaultIndicatorService @Inject constructor( return remember { derivedStateOf { - !canVerifySession && settingChatBackupIndicator.value + canVerifySession || settingChatBackupIndicator.value } } } From 4345f26d0bdf4038281349091743f185e4ddb461 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 16:43:09 +0100 Subject: [PATCH 07/27] Add a way to enter recovery key to verify the session. --- .../android/appnav/LoggedInFlowNode.kt | 26 ++++++++++--- features/securebackup/api/build.gradle.kts | 1 + .../api/SecureBackupEntryPoint.kt | 26 ++++++++++++- .../impl/DefaultSecureBackupEntryPoint.kt | 16 +++++++- .../securebackup/impl/SecureBackupFlowNode.kt | 6 ++- .../api/VerifySessionEntryPoint.kt | 18 ++++++++- .../impl/DefaultVerifySessionEntryPoint.kt | 16 +++++++- .../impl/VerifySelfSessionNode.kt | 10 +++++ .../impl/VerifySelfSessionPresenter.kt | 17 ++++++-- .../impl/VerifySelfSessionState.kt | 2 +- .../impl/VerifySelfSessionStateProvider.kt | 21 ++++++---- .../impl/VerifySelfSessionView.kt | 39 ++++++++++++++----- .../impl/VerifySelfSessionPresenterTests.kt | 35 ++++++++++++++--- .../src/main/res/values/localazy.xml | 1 + 14 files changed, 193 insertions(+), 41 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 3985128b21..9bb9231dc2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -216,7 +216,9 @@ class LoggedInFlowNode @AssistedInject constructor( data object VerifySession : NavTarget @Parcelize - data object SecureBackup : NavTarget + data class SecureBackup( + val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root + ) : NavTarget @Parcelize data object InviteList : NavTarget @@ -298,7 +300,7 @@ class LoggedInFlowNode @AssistedInject constructor( } override fun onSecureBackupClicked() { - backstack.push(NavTarget.SecureBackup) + backstack.push(NavTarget.SecureBackup()) } override fun onOpenRoomNotificationSettings(roomId: RoomId) { @@ -324,10 +326,24 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } NavTarget.VerifySession -> { - verifySessionEntryPoint.createNode(this, buildContext) + val callback = object : VerifySessionEntryPoint.Callback { + override fun onEnterRecoveryKey() { + backstack.replace( + NavTarget.SecureBackup( + initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey + ) + ) + } + } + verifySessionEntryPoint + .nodeBuilder(this, buildContext) + .callback(callback) + .build() } - NavTarget.SecureBackup -> { - secureBackupEntryPoint.createNode(this, buildContext) + is NavTarget.SecureBackup -> { + secureBackupEntryPoint.nodeBuilder(this, buildContext) + .params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement)) + .build() } NavTarget.InviteList -> { val callback = object : InviteListEntryPoint.Callback { diff --git a/features/securebackup/api/build.gradle.kts b/features/securebackup/api/build.gradle.kts index c9117d1d39..015cc600f2 100644 --- a/features/securebackup/api/build.gradle.kts +++ b/features/securebackup/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-library") + id("kotlin-parcelize") } android { diff --git a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt index 8824fdf84b..1fee6418a9 100644 --- a/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt +++ b/features/securebackup/api/src/main/kotlin/io/element/android/features/securebackup/api/SecureBackupEntryPoint.kt @@ -16,6 +16,28 @@ package io.element.android.features.securebackup.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import android.os.Parcelable +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.architecture.NodeInputs +import kotlinx.parcelize.Parcelize -interface SecureBackupEntryPoint : SimpleFeatureEntryPoint +interface SecureBackupEntryPoint : FeatureEntryPoint { + sealed interface InitialTarget : Parcelable { + @Parcelize + data object Root : InitialTarget + + @Parcelize + data object EnterRecoveryKey : InitialTarget + } + + data class Params(val initialElement: InitialTarget) : NodeInputs + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } +} diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt index a870255a9a..e3d5fde961 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/DefaultSecureBackupEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.securebackup.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,18 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultSecureBackupEntryPoint @Inject constructor() : SecureBackupEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SecureBackupEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : SecureBackupEntryPoint.NodeBuilder { + override fun params(params: SecureBackupEntryPoint.Params): SecureBackupEntryPoint.NodeBuilder { + plugins += params + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt index 62eecd09c2..172c6672e5 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/SecureBackupFlowNode.kt @@ -27,6 +27,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.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode @@ -44,7 +45,10 @@ class SecureBackupFlowNode @AssistedInject constructor( @Assisted plugins: List, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = when (plugins.filterIsInstance(SecureBackupEntryPoint.Params::class.java).first().initialElement) { + SecureBackupEntryPoint.InitialTarget.Root -> NavTarget.Root + SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey -> NavTarget.EnterRecoveryKey + }, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, diff --git a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt index 933ca1994f..5eb1bc8daa 100644 --- a/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt +++ b/features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/VerifySessionEntryPoint.kt @@ -16,6 +16,20 @@ package io.element.android.features.verifysession.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 VerifySessionEntryPoint : SimpleFeatureEntryPoint +interface VerifySessionEntryPoint : FeatureEntryPoint { + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onEnterRecoveryKey() + } +} diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt index da8c22e756..b514742a87 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.verifysession.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,18 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultVerifySessionEntryPoint @Inject constructor() : VerifySessionEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): VerifySessionEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : VerifySessionEntryPoint.NodeBuilder { + override fun callback(callback: VerifySessionEntryPoint.Callback): VerifySessionEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index 7612f9292c..c7369aab0e 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -21,9 +21,11 @@ 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.features.verifysession.api.VerifySessionEntryPoint import io.element.android.libraries.di.SessionScope @ContributesNode(SessionScope::class) @@ -32,12 +34,20 @@ class VerifySelfSessionNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: VerifySelfSessionPresenter, ) : Node(buildContext, plugins = plugins) { + + private fun onEnterRecoveryKey() { + plugins().forEach { + it.onEnterRecoveryKey() + } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() VerifySelfSessionView( state = state, modifier = modifier, + onEnterRecoveryKey = { onEnterRecoveryKey() }, goBack = { navigateUp() } ) } 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 4d6889f716..2a7740d8d0 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 @@ -20,12 +20,15 @@ package io.element.android.features.verifysession.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.matrix.api.encryption.EncryptionService +import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState import kotlinx.coroutines.CoroutineScope @@ -38,6 +41,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionStateMach class VerifySelfSessionPresenter @Inject constructor( private val sessionVerificationService: SessionVerificationService, + private val encryptionService: EncryptionService, private val stateMachine: VerifySelfSessionStateMachine, ) : Presenter { @Composable @@ -46,9 +50,14 @@ class VerifySelfSessionPresenter @Inject constructor( // 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() val verificationFlowStep by remember { - derivedStateOf { stateAndDispatch.state.value.toVerificationStep() } + derivedStateOf { + stateAndDispatch.state.value.toVerificationStep( + canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE + ) + } } // Start this after observing state machine LaunchedEffect(Unit) { @@ -71,10 +80,12 @@ class VerifySelfSessionPresenter @Inject constructor( ) } - private fun StateMachineState?.toVerificationStep(): VerifySelfSessionState.VerificationStep = + private fun StateMachineState?.toVerificationStep( + canEnterRecoveryKey: Boolean + ): VerifySelfSessionState.VerificationStep = when (val machineState = this) { StateMachineState.Initial, null -> { - VerifySelfSessionState.VerificationStep.Initial + VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = canEnterRecoveryKey) } StateMachineState.RequestingVerification, StateMachineState.StartingSasVerification, 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 1273367a71..fa3cb68adf 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 @@ -28,7 +28,7 @@ data class VerifySelfSessionState( ) { @Stable sealed interface VerificationStep { - data object Initial : VerificationStep + data class Initial(val canEnterRecoveryKey: Boolean) : VerificationStep data object Canceled : VerificationStep data object AwaitingOtherDeviceResponse : VerificationStep data object Ready : 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 81f25866bd..eeaf5c4e38 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 @@ -25,24 +25,27 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider get() = sequenceOf( aVerifySelfSessionState(), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized) ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading()) ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready ), - aVerifySelfSessionState().copy( + aVerifySelfSessionState( verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized) ), + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true) + ), // Add other state here ) } @@ -59,8 +62,10 @@ private fun aDecimalsSessionVerificationData( return SessionVerificationData.Decimals(decimals) } -private fun aVerifySelfSessionState() = VerifySelfSessionState( - verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial, +private fun aVerifySelfSessionState( + verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false), +) = VerifySelfSessionState( + verificationFlowStep = verificationFlowStep, 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 e71df4c6d1..871825539a 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 @@ -62,6 +62,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver fun VerifySelfSessionView( state: VerifySelfSessionState, modifier: Modifier = Modifier, + onEnterRecoveryKey: () -> Unit, goBack: () -> Unit, ) { fun goBackAndCancelIfNeeded() { @@ -85,7 +86,11 @@ fun VerifySelfSessionView( }, footer = { if (buttonsVisible) { - BottomMenu(screenState = state, goBack = ::goBackAndCancelIfNeeded) + BottomMenu( + screenState = state, + goBack = ::goBackAndCancelIfNeeded, + onEnterRecoveryKey = onEnterRecoveryKey + ) } } ) { @@ -96,13 +101,13 @@ fun VerifySelfSessionView( @Composable private fun HeaderContent(verificationFlowStep: FlowStep) { val iconResourceId = when (verificationFlowStep) { - FlowStep.Initial -> R.drawable.ic_verification_devices + is FlowStep.Initial -> R.drawable.ic_verification_devices FlowStep.Canceled -> R.drawable.ic_verification_warning FlowStep.AwaitingOtherDeviceResponse -> R.drawable.ic_verification_waiting FlowStep.Ready, is FlowStep.Verifying, FlowStep.Completed -> R.drawable.ic_verification_emoji } val titleTextId = when (verificationFlowStep) { - FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title + is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_title FlowStep.Canceled -> CommonStrings.common_verification_cancelled FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_title FlowStep.Ready, @@ -113,7 +118,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { } } val subtitleTextId = when (verificationFlowStep) { - FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle + is FlowStep.Initial -> R.string.screen_session_verification_open_existing_session_subtitle FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_session_verification_waiting_to_accept_subtitle FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle @@ -136,7 +141,7 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { private fun Content(flowState: FlowStep) { Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { when (flowState) { - FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit + is FlowStep.Initial, FlowStep.Ready, FlowStep.Canceled, FlowStep.Completed -> Unit FlowStep.AwaitingOtherDeviceResponse -> ContentWaiting() is FlowStep.Verifying -> ContentVerifying(flowState) } @@ -203,13 +208,17 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie } @Composable -private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) { +private fun BottomMenu( + screenState: VerifySelfSessionState, + onEnterRecoveryKey: () -> Unit, + goBack: () -> Unit, +) { val verificationViewState = screenState.verificationFlowStep val eventSink = screenState.eventSink val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading val positiveButtonTitle = when (verificationViewState) { - FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial + is FlowStep.Initial -> R.string.screen_session_verification_positive_button_initial FlowStep.Canceled -> R.string.screen_session_verification_positive_button_canceled is FlowStep.Verifying -> { if (isVerifying) { @@ -222,7 +231,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) else -> null } val negativeButtonTitle = when (verificationViewState) { - FlowStep.Initial -> CommonStrings.action_cancel + is FlowStep.Initial -> CommonStrings.action_cancel FlowStep.Canceled -> CommonStrings.action_cancel is FlowStep.Verifying -> R.string.screen_session_verification_they_dont_match else -> null @@ -230,7 +239,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) val negativeButtonEnabled = !isVerifying val positiveButtonEvent = when (verificationViewState) { - FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification + is FlowStep.Initial -> VerifySelfSessionViewEvents.RequestVerification FlowStep.Ready -> VerifySelfSessionViewEvents.StartSasVerification is FlowStep.Verifying -> if (!isVerifying) VerifySelfSessionViewEvents.ConfirmVerification else null FlowStep.Canceled -> VerifySelfSessionViewEvents.Restart @@ -263,6 +272,17 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) enabled = negativeButtonEnabled, ) } + if (verificationViewState is FlowStep.Initial && verificationViewState.canEnterRecoveryKey) { + Text( + text = stringResource(id = CommonStrings.common_or), + color = ElementTheme.colors.textSecondary, + ) + TextButton( + text = stringResource(R.string.screen_session_verification_enter_recovery_key), + modifier = Modifier.fillMaxWidth(), + onClick = onEnterRecoveryKey, + ) + } } } @@ -271,6 +291,7 @@ private fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit) internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { VerifySelfSessionView( state = state, + onEnterRecoveryKey = {}, goBack = {}, ) } 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 240a4f0606..ad128b450d 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,9 +23,13 @@ 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.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.VerificationEmoji import io.element.android.libraries.matrix.api.verification.VerificationFlowState +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 kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,7 +48,21 @@ class VerifySelfSessionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) + } + } + + @Test + fun `present - Initial state is received, can use recovery key`() = runTest { + val presenter = createVerifySelfSessionPresenter( + encryptionService = FakeEncryptionService().apply { + emitRecoveryState(RecoveryState.INCOMPLETE) + } + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true)) } } @@ -67,7 +85,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.StartSasVerification) // Await for other device response: @@ -86,7 +104,7 @@ class VerifySelfSessionPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) val eventSink = initialState.eventSink eventSink(VerifySelfSessionViewEvents.CancelAndClose) expectNoEvents() @@ -203,7 +221,7 @@ class VerifySelfSessionPresenterTests { sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()), ): VerifySelfSessionState { var state = awaitItem() - assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial) + assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false)) state.eventSink(VerifySelfSessionViewEvents.RequestVerification) // Await for other device response: state = awaitItem() @@ -223,8 +241,13 @@ class VerifySelfSessionPresenterTests { } private fun createVerifySelfSessionPresenter( - service: FakeSessionVerificationService = FakeSessionVerificationService() + service: SessionVerificationService = FakeSessionVerificationService(), + encryptionService: EncryptionService = FakeEncryptionService(), ): VerifySelfSessionPresenter { - return VerifySelfSessionPresenter(service, VerifySelfSessionStateMachine(service)) + return VerifySelfSessionPresenter( + sessionVerificationService = service, + encryptionService = encryptionService, + stateMachine = VerifySelfSessionStateMachine(service), + ) } } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 77248a2339..85a2e78a85 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -141,6 +141,7 @@ "Mute" "No results" "Offline" + "or" "Password" "People" "Permalink" From 4990aa38d3efa820cb9c708bed0bf28022a53474 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 17:36:53 +0100 Subject: [PATCH 08/27] Do not show the RecoveryKeyConfirmation banner if the sync is not Running. --- .../android/features/roomlist/impl/RoomListPresenter.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 1467595de0..9c3c154cb3 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -51,6 +51,8 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId 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.SyncService +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.getCurrentUser @@ -87,6 +89,7 @@ class RoomListPresenter @Inject constructor( ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() private val sessionVerificationService: SessionVerificationService = client.sessionVerificationService() + private val syncService: SyncService = client.syncService() @Composable override fun present(): RoomListState { @@ -115,6 +118,7 @@ class RoomListPresenter @Inject constructor( isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false } val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + val syncState by syncService.syncState.collectAsState() val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage).collectAsState(initial = null) val securityBannerState by remember { derivedStateOf { @@ -125,7 +129,9 @@ class RoomListPresenter @Inject constructor( } else { SecurityBannerState.SessionVerification } - secureStorageFlag == true && recoveryState == RecoveryState.INCOMPLETE -> SecurityBannerState.RecoveryKeyConfirmation + secureStorageFlag == true && + recoveryState == RecoveryState.INCOMPLETE && + syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation else -> SecurityBannerState.None } } From 347edb67ab243a924cf16c35eb204516126039e6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 17:48:08 +0100 Subject: [PATCH 09/27] Fix tests. --- .../roomlist/impl/RoomListPresenterTests.kt | 13 ++++++++----- .../libraries/matrix/test/sync/FakeSyncService.kt | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 8b937db63d..698408d6e1 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -54,6 +54,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.roomlist.RoomListService +import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -68,6 +69,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService +import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService @@ -103,7 +105,7 @@ class RoomListPresenterTests { assertThat(withUserState.matrixUser!!.userId).isEqualTo(A_USER_ID) assertThat(withUserState.matrixUser!!.displayName).isEqualTo(A_USER_NAME) assertThat(withUserState.matrixUser!!.avatarUrl).isEqualTo(AN_AVATAR_URL) - assertThat(withUserState.showAvatarIndicator).isFalse() + assertThat(withUserState.showAvatarIndicator).isTrue() scope.cancel() } } @@ -126,12 +128,12 @@ class RoomListPresenterTests { }.test { skipItems(1) val initialState = awaitItem() - assertThat(initialState.showAvatarIndicator).isFalse() + assertThat(initialState.showAvatarIndicator).isTrue() sessionVerificationService.givenCanVerifySession(false) - assertThat(awaitItem().showAvatarIndicator).isFalse() - encryptionService.emitBackupState(BackupState.UNKNOWN) + assertThat(awaitItem().showAvatarIndicator).isTrue() + encryptionService.emitBackupState(BackupState.ENABLED) val finalState = awaitItem() - assertThat(finalState.showAvatarIndicator).isTrue() + assertThat(finalState.showAvatarIndicator).isFalse() scope.cancel() } } @@ -284,6 +286,7 @@ class RoomListPresenterTests { sessionVerificationService = FakeSessionVerificationService().apply { givenCanVerifySession(false) }, + syncService = FakeSyncService(initialState = SyncState.Running) ) val scope = CoroutineScope(context = coroutineContext + SupervisorJob()) val presenter = createRoomListPresenter( diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt index d140bafd6f..ffc06e7d18 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt @@ -21,8 +21,10 @@ import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class FakeSyncService : SyncService { - private val syncStateFlow = MutableStateFlow(SyncState.Idle) +class FakeSyncService( + initialState: SyncState = SyncState.Idle +) : SyncService { + private val syncStateFlow = MutableStateFlow(initialState) fun simulateError() { syncStateFlow.value = SyncState.Error From 600557fd53af8a99cb1a6f125eac292af17554e3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 20 Feb 2024 18:08:12 +0100 Subject: [PATCH 10/27] Remove unused val. --- .../android/features/location/impl/show/ShowLocationViewTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt index a66197e733..05160dca56 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt @@ -78,7 +78,6 @@ class ShowLocationViewTest { ), onBackPressed = EnsureNeverCalled(), ) - val shareContentDescription = rule.activity.getString(CommonStrings.action_share) rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true)) } From a495548ada5ef46ac52610ff0cad654dd0807646 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 20 Feb 2024 17:41:24 +0000 Subject: [PATCH 11/27] Update screenshots --- ..._ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png | 4 ++-- ...onfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png | 4 ++-- ...tView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...iew_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...kupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...kupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...kupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...kupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...pEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...pEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...pEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...pEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ll_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png | 4 ++-- ..._SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...l_VerifySelfSessionView-Day-0_1_null_7,NEXUS_5,1.0,en].png | 3 +++ ...VerifySelfSessionView-Night-0_2_null_7,NEXUS_5,1.0,en].png | 3 +++ 16 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 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 create mode 100644 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 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png index f8aa6033e7..0b1bbd56bd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Day-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:392e3aeea8550204b0fb40e6c0c01faa113830d9566343c55ceba8b3040c0dd3 -size 29459 +oid sha256:fed9d57b66ccd88f3ce45c63f81fec72cd3622bb3e15380812dda629dca0b666 +size 29066 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png index f09e4db204..eae99afbbf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl.components_ConfirmRecoveryKeyBanner_null_ConfirmRecoveryKeyBanner-Night-4_6_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a00e061ea1c7bc7fa842fe23ddd639bbab526fa78e1937eeb54edc551bb6b9be -size 28873 +oid sha256:4057457aabe727afb1c74401da6d7f1cd6ddbc453f73cc6754e5a97c566e8a9a +size 28523 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png index 4e360c7666..1429d83081 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Day-3_4_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f8802121f779f48dd556a9a3a08e6ac1a63e86d5695f6f8c133fbb346d76ef6 -size 89779 +oid sha256:4ffaf1a24c2adc5a16e2b835b012bb8d4952bb503de4d1303449235e921a6f17 +size 89209 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png index a751592e9b..b797d9d35e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.roomlist.impl_RoomListView_null_RoomListView-Night-3_5_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cdd93c157565f7f8d3fa23a43c30e02531dab40578a315c5d0d10a99e74f259 -size 91394 +oid sha256:ca95a64ab30cd9118b81edd08026371f66eeeb0c5fc11dbdc8a756f80465331a +size 90996 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png index 6732b1d851..c149b61eba 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4e895003e43ad5a7fae10cfeeae35ec0443af0f27577dc6ff23b000835700e6 -size 34051 +oid sha256:92f023f5edc4c050932f8a9e3a3f886285ef9cc716e999cc66f32101a58f295d +size 33356 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png index 6327827ffc..5d53c122c6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81f8b6a636a32c0fa71bb3db89ab1489b16b95b066e41e91bc8f4e1f41f33d04 -size 46077 +oid sha256:380e3d8df049edf96053533d8d22b8bd46215a6961c6d2b6ca6d151b47c2201e +size 45361 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png index c28b5835ed..3a7c3ae48c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:711a565771c8c46474b8a719098453ab753dcbadc573390fdd5c067138dc628c -size 44466 +oid sha256:713d30ae1e8896a410396335ad72dbf75d207096a59caf90e6d3135d4cd55254 +size 43747 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png index 2d4da1f4a4..7e4da73521 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Day-2_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69e430ad2e73557c2ca2dbdb3137ba392644a1d2f36146a1ffebdc14ff3eb7a2 -size 43548 +oid sha256:d3f640e29d7029a278172b45e0597e45653beb58f195bcf71f3fc31c8832708b +size 42973 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png index eda4f29921..11bf947e5e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b5beac400ad75bdc6e260e9b653c52108485a3eabb553f05681407cd9e99b2b -size 32408 +oid sha256:58529ffb570bf24dffc2bbc019de2d108467d577ce5b699a09ab62ee22882bdc +size 31854 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png index 6f6682950f..c1f40a5686 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67dcdd1627f9e4ac883f7c453d3b7f728d889d2f97541584a02fe563b223fe73 -size 42869 +oid sha256:933b83af0d608e39b8d97a3bedee82040a5a13889e23dce7802955488e0fe227 +size 42290 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png index d73027376e..47008a7b67 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bc8998cb3def15c194bca0ef55b59d15aebcc992baddd8e5a040b86b4e64dd7 -size 41460 +oid sha256:62ae02108b8bf661d7f799b64d5e6c1794acb00f1c91a50086fd279fe6ea62d4 +size 40908 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png index 1cab95a338..52594d6e32 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.enter_SecureBackupEnterRecoveryKeyView_null_SecureBackupEnterRecoveryKeyView-Night-2_4_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36d192e6fc4ce53fbe5cfc7d1880333cad8727cd18d2472f51ebe69f3afe2396 -size 38506 +oid sha256:24285b1233ff3695d940ee3025b45e83a2b6094f17c9c6b65e8f1bbdccc7b7c8 +size 37961 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png index 3c2c7ec9f6..d44ab16581 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Day-3_4_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75a5fa36d754f794c08f8af3719b9053c05567e75cde2f7723ac4b001e7091fa -size 31848 +oid sha256:501e818e04fe0839ffde3b2860c1fa8ea5773f794b7bbe10d6625d15d2d9b193 +size 31309 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png index 58d49595c5..7677836d5a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.securebackup.impl.root_SecureBackupRootView_null_SecureBackupRootView-Night-3_5_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:197a85cabda2d8b733768f33ce0883d260671111ae6fa69f58dd4dd40485248c -size 29924 +oid sha256:9e04b234f7f8ad17638f4dec7d1f8edae6f5ca05f85074793718591da7066e89 +size 29439 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 new file mode 100644 index 0000000000..609952770e --- /dev/null +++ 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 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cbaaf6ee91626a9e8db83ce42be14e1a02a8c841230bf7829fa74aff8cbdfab +size 32513 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 new file mode 100644 index 0000000000..cb997ffa7d --- /dev/null +++ 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 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efcf672eba104371f27b43c9204d207f11f9ce42098da0e4dce3da4aa6639153 +size 30517 From 99b8efbeff543dccb261eddbae85edd39d0f8a02 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 09:43:24 +0100 Subject: [PATCH 12/27] Go directly to the enter recovery key screen when coming from the banner which ask the user to do so. --- .../kotlin/io/element/android/appnav/LoggedInFlowNode.kt | 4 ++++ .../android/features/roomlist/api/RoomListEntryPoint.kt | 1 + .../element/android/features/roomlist/impl/RoomListNode.kt | 5 +++++ .../element/android/features/roomlist/impl/RoomListView.kt | 6 +++++- .../io/element/android/samples/minimal/RoomListScreen.kt | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) 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 9bb9231dc2..db75acff64 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -255,6 +255,10 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push(NavTarget.VerifySession) } + override fun onSessionConfirmRecoveryKeyClicked() { + backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey)) + } + override fun onInvitesClicked() { backstack.push(NavTarget.InviteList) } diff --git a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt index 1b357c794c..0404ce18fc 100644 --- a/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt +++ b/features/roomlist/api/src/main/kotlin/io/element/android/features/roomlist/api/RoomListEntryPoint.kt @@ -34,6 +34,7 @@ interface RoomListEntryPoint : FeatureEntryPoint { fun onCreateRoomClicked() fun onSettingsClicked() fun onSessionVerificationClicked() + fun onSessionConfirmRecoveryKeyClicked() fun onInvitesClicked() fun onRoomSettingsClicked(roomId: RoomId) fun onReportBugClicked() diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index f505020151..5dec53e158 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -68,6 +68,10 @@ class RoomListNode @AssistedInject constructor( plugins().forEach { it.onSessionVerificationClicked() } } + private fun onSessionConfirmRecoveryKeyClicked() { + plugins().forEach { it.onSessionConfirmRecoveryKeyClicked() } + } + private fun onInvitesClicked() { plugins().forEach { it.onInvitesClicked() } } @@ -97,6 +101,7 @@ class RoomListNode @AssistedInject constructor( onSettingsClicked = this::onOpenSettings, onCreateRoomClicked = this::onCreateRoomClicked, onVerifyClicked = this::onSessionVerificationClicked, + onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked, onInvitesClicked = this::onInvitesClicked, onRoomSettingsClicked = this::onRoomSettingsClicked, onMenuActionClicked = { onMenuActionClicked(activity, it) }, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 575ace8cf7..5ca8085fae 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -78,6 +78,7 @@ fun RoomListView( onRoomClicked: (RoomId) -> Unit, onSettingsClicked: () -> Unit, onVerifyClicked: () -> Unit, + onConfirmRecoveryKeyClicked: () -> Unit, onCreateRoomClicked: () -> Unit, onInvitesClicked: () -> Unit, onRoomSettingsClicked: (roomId: RoomId) -> Unit, @@ -109,6 +110,7 @@ fun RoomListView( modifier = Modifier.padding(top = topPadding), state = state, onVerifyClicked = onVerifyClicked, + onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onRoomClicked = onRoomClicked, onRoomLongClicked = { onRoomLongClicked(it) }, onOpenSettings = onSettingsClicked, @@ -166,6 +168,7 @@ private fun EmptyRoomListView( private fun RoomListContent( state: RoomListState, onVerifyClicked: () -> Unit, + onConfirmRecoveryKeyClicked: () -> Unit, onRoomClicked: (RoomId) -> Unit, onRoomLongClicked: (RoomListRoomSummary) -> Unit, onOpenSettings: () -> Unit, @@ -238,7 +241,7 @@ private fun RoomListContent( SecurityBannerState.RecoveryKeyConfirmation -> { item { ConfirmRecoveryKeyBanner( - onContinueClicked = onOpenSettings, + onContinueClicked = onConfirmRecoveryKeyClicked, onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } ) } @@ -304,6 +307,7 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class) onRoomClicked = {}, onSettingsClicked = {}, onVerifyClicked = {}, + onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, onInvitesClicked = {}, onRoomSettingsClicked = {}, diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 1d5b27f731..1730ff1e90 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -142,6 +142,7 @@ class RoomListScreen( onRoomClicked = ::onRoomClicked, onSettingsClicked = {}, onVerifyClicked = {}, + onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, onInvitesClicked = {}, onRoomSettingsClicked = {}, From daa90a06605137f43d128433bce7330b100483d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 10:07:41 +0100 Subject: [PATCH 13/27] Remove feature flag `SecureStorage` --- .../features/logout/impl/LogoutPresenter.kt | 16 ++-------------- .../impl/direct/DefaultDirectLogoutPresenter.kt | 16 ++-------------- .../features/logout/impl/LogoutPresenterTest.kt | 4 ---- .../direct/DefaultDirectLogoutPresenterTest.kt | 12 ++++-------- .../impl/root/PreferencesRootPresenter.kt | 5 +---- .../impl/root/PreferencesRootPresenterTest.kt | 1 - .../features/roomlist/impl/RoomListPresenter.kt | 4 +--- .../roomlist/impl/RoomListPresenterTests.kt | 4 +--- .../libraries/featureflag/api/FeatureFlags.kt | 7 ------- .../impl/StaticFeatureFlagProvider.kt | 1 - .../indicator/impl/DefaultIndicatorService.kt | 7 +------ 11 files changed, 12 insertions(+), 65 deletions(-) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index f56a24cef5..851e76a32a 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -30,22 +30,17 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.core.bool.orTrue -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject class LogoutPresenter @Inject constructor( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, - private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): LogoutState { @@ -54,15 +49,8 @@ class LogoutPresenter @Inject constructor( mutableStateOf(AsyncAction.Uninitialized) } - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - - val backupUploadState: BackupUploadState by remember(secureStorageFlag) { - when (secureStorageFlag) { - true -> encryptionService.waitForBackupUploadSteadyState() - false -> flowOf(BackupUploadState.Done) - else -> emptyFlow() - } + val backupUploadState: BackupUploadState by remember { + encryptionService.waitForBackupUploadSteadyState() } .collectAsState(initial = BackupUploadState.Unknown) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt index d9a6e7e94c..4cf2445142 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt @@ -33,14 +33,10 @@ import io.element.android.features.logout.impl.tools.isBackingUp import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject @@ -48,7 +44,6 @@ import javax.inject.Inject class DefaultDirectLogoutPresenter @Inject constructor( private val matrixClient: MatrixClient, private val encryptionService: EncryptionService, - private val featureFlagService: FeatureFlagService, ) : DirectLogoutPresenter { @Composable override fun present(): DirectLogoutState { @@ -58,15 +53,8 @@ class DefaultDirectLogoutPresenter @Inject constructor( mutableStateOf(AsyncAction.Uninitialized) } - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - - val backupUploadState: BackupUploadState by remember(secureStorageFlag) { - when (secureStorageFlag) { - true -> encryptionService.waitForBackupUploadSteadyState() - false -> flowOf(BackupUploadState.Done) - else -> emptyFlow() - } + val backupUploadState: BackupUploadState by remember { + encryptionService.waitForBackupUploadSteadyState() } .collectAsState(initial = BackupUploadState.Unknown) diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index 8fab9f3e01..fbf08bc723 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -22,8 +22,6 @@ import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupUploadState @@ -99,7 +97,6 @@ class LogoutPresenterTest { assertThat(initialState.isLastDevice).isFalse() assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) - skipItems(1) val waitingState = awaitItem() assertThat(waitingState.backupUploadState).isEqualTo(BackupUploadState.Waiting) skipItems(1) @@ -209,6 +206,5 @@ class LogoutPresenterTest { ): LogoutPresenter = LogoutPresenter( matrixClient = matrixClient, encryptionService = encryptionService, - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), ) } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt index 50a1e381e8..0c3aa3131d 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt @@ -23,8 +23,6 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -63,8 +61,8 @@ class DefaultDirectLogoutPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) - val initialState = awaitItem() + skipItems(1) + val initialState = awaitFirstItem() assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -84,8 +82,8 @@ class DefaultDirectLogoutPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) - val initialState = awaitItem() + skipItems(1) + val initialState = awaitFirstItem() assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -180,7 +178,6 @@ class DefaultDirectLogoutPresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(1) return awaitItem() } @@ -190,6 +187,5 @@ class DefaultDirectLogoutPresenterTest { ): DefaultDirectLogoutPresenter = DefaultDirectLogoutPresenter( matrixClient = matrixClient, encryptionService = encryptionService, - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), ) } 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 067bd2a26f..434985e068 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 @@ -79,9 +79,6 @@ class PreferencesRootPresenter @Inject constructor( val showSecureBackupIndicator by indicatorService.showSettingChatBackupIndicator() - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) - val accountManagementUrl: MutableState = remember { mutableStateOf(null) } @@ -101,7 +98,7 @@ class PreferencesRootPresenter @Inject constructor( version = versionFormatter.get(), deviceId = matrixClient.deviceId, showCompleteVerification = showCompleteVerification, - showSecureBackup = !showCompleteVerification && secureStorageFlag == true, + showSecureBackup = !showCompleteVerification, showSecureBackupBadge = showSecureBackupIndicator, accountManagementUrl = accountManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 2dea13eac3..f9789639f3 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -65,7 +65,6 @@ class PreferencesRootPresenterTest { indicatorService = DefaultIndicatorService( sessionVerificationService = sessionVerificationService, encryptionService = FakeEncryptionService(), - featureFlagService = FakeFeatureFlagService(), ), directLogoutPresenter = object : DirectLogoutPresenter { @Composable diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 9c3c154cb3..b32dfc44f9 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -119,7 +119,6 @@ class RoomListPresenter @Inject constructor( } val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val syncState by syncService.syncState.collectAsState() - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage).collectAsState(initial = null) val securityBannerState by remember { derivedStateOf { when { @@ -129,8 +128,7 @@ class RoomListPresenter @Inject constructor( } else { SecurityBannerState.SessionVerification } - secureStorageFlag == true && - recoveryState == RecoveryState.INCOMPLETE && + recoveryState == RecoveryState.INCOMPLETE && syncState == SyncState.Running -> SecurityBannerState.RecoveryKeyConfirmation else -> SecurityBannerState.None } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 698408d6e1..6b714c4841 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -45,7 +45,6 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.libraries.indicator.impl.DefaultIndicatorService @@ -606,7 +605,7 @@ class RoomListPresenterTests { }, roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), - featureFlagService: FeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SecureStorage.key to true)), + featureFlagService: FeatureFlagService = FakeFeatureFlagService(), coroutineScope: CoroutineScope, migrationScreenPresenter: MigrationScreenPresenter = MigrationScreenPresenter( matrixClient = client, @@ -634,7 +633,6 @@ class RoomListPresenterTests { indicatorService = DefaultIndicatorService( sessionVerificationService = client.sessionVerificationService(), encryptionService = client.encryptionService(), - featureFlagService = featureFlagService, ), migrationScreenPresenter = migrationScreenPresenter, searchPresenter = searchPresenter, diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 3c56c81fae..fea9088c2a 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -68,13 +68,6 @@ enum class FeatureFlags( defaultValue = true, isFinished = false, ), - SecureStorage( - key = "feature.securestorage", - title = "Chat backup", - description = "Allow access to backup and restore chat history settings", - defaultValue = true, - isFinished = false, - ), MarkAsUnread( key = "feature.markAsUnread", title = "Mark as unread", diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 8462a33ba5..757f01904f 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -39,7 +39,6 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.VoiceMessages -> true FeatureFlags.PinUnlock -> true FeatureFlags.Mentions -> true - FeatureFlags.SecureStorage -> true FeatureFlags.MarkAsUnread -> false } } else { diff --git a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt index 265e3d2d6f..aae0f1f913 100644 --- a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt +++ b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.indicator.api.IndicatorService import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -37,7 +35,6 @@ import javax.inject.Inject class DefaultIndicatorService @Inject constructor( private val sessionVerificationService: SessionVerificationService, private val encryptionService: EncryptionService, - private val featureFlagService: FeatureFlagService, ) : IndicatorService { @Composable override fun showRoomListTopBarIndicator(): State { @@ -53,8 +50,6 @@ class DefaultIndicatorService @Inject constructor( @Composable override fun showSettingChatBackupIndicator(): State { - val secureStorageFlag by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SecureStorage) - .collectAsState(initial = null) val backupState by encryptionService.backupStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() @@ -67,7 +62,7 @@ class DefaultIndicatorService @Inject constructor( RecoveryState.DISABLED, RecoveryState.INCOMPLETE, ) - secureStorageFlag == true && (showForBackup || showForRecovery) + showForBackup || showForRecovery } } } From 6ddc2f58140265dc48e86530eb02e87fab4c446d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 10:33:08 +0100 Subject: [PATCH 14/27] Make isLastSession "live" --- .../features/logout/impl/LogoutPresenter.kt | 7 +------ .../direct/DefaultDirectLogoutPresenter.kt | 7 +------ .../logout/impl/LogoutPresenterTest.kt | 4 ++-- .../DefaultDirectLogoutPresenterTest.kt | 3 +-- .../roomlist/impl/RoomListPresenter.kt | 5 +---- .../roomlist/impl/RoomListPresenterTests.kt | 4 ++-- .../api/encryption/EncryptionService.kt | 3 +-- .../impl/encryption/RustEncryptionService.kt | 21 ++++++++++++++++++- .../test/encryption/FakeEncryptionService.kt | 11 +++------- 9 files changed, 32 insertions(+), 33 deletions(-) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index 851e76a32a..34f3ca84a6 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -24,7 +24,6 @@ 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 io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -54,11 +53,7 @@ class LogoutPresenter @Inject constructor( } .collectAsState(initial = BackupUploadState.Unknown) - var isLastDevice by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false - } - + val isLastDevice by encryptionService.isLastDevice.collectAsState() val backupState by encryptionService.backupStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt index 4cf2445142..b0924e61e0 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenter.kt @@ -17,14 +17,12 @@ package io.element.android.features.logout.impl.direct import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState 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.squareup.anvil.annotations.ContributesBinding import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutPresenter @@ -58,10 +56,7 @@ class DefaultDirectLogoutPresenter @Inject constructor( } .collectAsState(initial = BackupUploadState.Unknown) - var isLastDevice by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false - } + val isLastDevice by encryptionService.isLastDevice.collectAsState() fun handleEvents(event: DirectLogoutEvents) { when (event) { diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index fbf08bc723..9531ea8886 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -61,13 +61,13 @@ class LogoutPresenterTest { fun `present - initial state - last session`() = runTest { val presenter = createLogoutPresenter( encryptionService = FakeEncryptionService().apply { - givenIsLastDevice(true) + emitIsLastDevice(true) } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(3) + skipItems(2) val initialState = awaitItem() assertThat(initialState.isLastDevice).isTrue() assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt index 0c3aa3131d..bf3df93731 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutPresenterTest.kt @@ -55,13 +55,12 @@ class DefaultDirectLogoutPresenterTest { fun `present - initial state - last session`() = runTest { val presenter = createDefaultDirectLogoutPresenter( encryptionService = FakeEncryptionService().apply { - givenIsLastDevice(true) + emitIsLastDevice(true) } ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitFirstItem() assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index b32dfc44f9..69a1b4cf9a 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -113,10 +113,7 @@ class RoomListPresenter @Inject constructor( var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) - var isLastDevice by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false - } + val isLastDevice by encryptionService.isLastDevice.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val syncState by syncService.syncState.collectAsState() val securityBannerState by remember { diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index 6b714c4841..7fe08975e1 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -243,14 +243,14 @@ class RoomListPresenterTests { coroutineScope = scope, client = FakeMatrixClient( encryptionService = FakeEncryptionService().apply { - givenIsLastDevice(true) + emitIsLastDevice(true) } ), ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) + skipItems(1) val eventSink = awaitItem().eventSink // For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index 522445b6c2..36c786a26f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -23,11 +23,10 @@ interface EncryptionService { val backupStateStateFlow: StateFlow val recoveryStateStateFlow: StateFlow val enableRecoveryProgressStateFlow: StateFlow + val isLastDevice: StateFlow suspend fun enableBackups(): Result - suspend fun isLastDevice(): Result - /** * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. */ 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 b0c33949d3..282e5814ef 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 @@ -27,12 +27,17 @@ import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.impl.sync.RustSyncService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.currentCoroutineContext +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.callbackFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.BackupStateListener import org.matrix.rustcomponents.sdk.BackupSteadyStateListener @@ -88,6 +93,20 @@ internal class RustEncryptionService( override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) + /** + * Check if the session is the last session every 5 seconds. + * TODO This is a temporary workaround, when we will have a way to observe + * the sessions, this code will have to be updated. + */ + override val isLastDevice: StateFlow = flow { + while (currentCoroutineContext().isActive) { + val result = isLastDevice().getOrDefault(false) + emit(result) + delay(5_000) + } + } + .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) + fun start() { service.backupStateListener(object : BackupStateListener { override fun onUpdate(status: RustBackupState) { @@ -173,7 +192,7 @@ internal class RustEncryptionService( } } - override suspend fun isLastDevice(): Result = withContext(dispatchers.io) { + private suspend fun isLastDevice(): Result = withContext(dispatchers.io) { runCatching { service.isLastDevice() }.mapFailure { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt index 7593a5ba51..cc7f53eca3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeEncryptionService.kt @@ -31,6 +31,7 @@ class FakeEncryptionService : EncryptionService { override val backupStateStateFlow: MutableStateFlow = MutableStateFlow(BackupState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow = MutableStateFlow(RecoveryState.UNKNOWN) override val enableRecoveryProgressStateFlow: MutableStateFlow = MutableStateFlow(EnableRecoveryProgress.Starting) + override val isLastDevice: MutableStateFlow = MutableStateFlow(false) private var waitForBackupUploadSteadyStateFlow: Flow = flowOf() private var recoverFailure: Exception? = null @@ -73,14 +74,8 @@ class FakeEncryptionService : EncryptionService { return Result.success(Unit) } - private var isLastDevice = false - - fun givenIsLastDevice(isLastDevice: Boolean) { - this.isLastDevice = isLastDevice - } - - override suspend fun isLastDevice(): Result = simulateLongTask { - return Result.success(isLastDevice) + fun emitIsLastDevice(isLastDevice: Boolean) { + this.isLastDevice.value = isLastDevice } override suspend fun resetRecoveryKey(): Result = simulateLongTask { From 661b9d8653cb0b4f119f70bf97a2e24a2b9ecb4a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 11:24:35 +0100 Subject: [PATCH 15/27] Keep references on TaskHandle, to avoid it to be garbage collected, and so the listeners are stopped. --- .../matrix/impl/encryption/RustEncryptionService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 282e5814ef..84e9fb2f14 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.matrix.impl.util.cancelAndDestroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.currentCoroutineContext @@ -45,6 +46,7 @@ import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener import org.matrix.rustcomponents.sdk.Encryption import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.BackupState as RustBackupState import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress @@ -64,6 +66,8 @@ internal class RustEncryptionService( private val enableRecoveryProgressMapper = EnableRecoveryProgressMapper() private val backupUploadStateMapper = BackupUploadStateMapper() private val steadyStateExceptionMapper = SteadyStateExceptionMapper() + private var backupStateListenerTaskHandle: TaskHandle? = null + private var recoveryStateListenerTaskHandle: TaskHandle? = null private val backupStateFlow = MutableStateFlow(service.backupState().let(backupStateMapper::map)) @@ -108,13 +112,13 @@ internal class RustEncryptionService( .stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false) fun start() { - service.backupStateListener(object : BackupStateListener { + backupStateListenerTaskHandle = service.backupStateListener(object : BackupStateListener { override fun onUpdate(status: RustBackupState) { backupStateFlow.value = backupStateMapper.map(status) } }) - service.recoveryStateListener(object : RecoveryStateListener { + recoveryStateListenerTaskHandle = service.recoveryStateListener(object : RecoveryStateListener { override fun onUpdate(status: RustRecoveryState) { recoveryStateFlow.value = recoveryStateMapper.map(status) } @@ -122,7 +126,8 @@ internal class RustEncryptionService( } fun destroy() { - // No way to remove the listeners... + backupStateListenerTaskHandle?.cancelAndDestroy() + recoveryStateListenerTaskHandle?.cancelAndDestroy() service.destroy() } From 0927821219d5bb900cdfa4eef06555b27d6e29d9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 14:23:49 +0100 Subject: [PATCH 16/27] Force a refresh of the verification status when the recovery state value change. The session can become verified when the user enters their recovery key, and in this case the callback `didFinish` is not invoked. --- .../libraries/matrix/impl/RustMatrixClient.kt | 6 ++++- .../RustSessionVerificationService.kt | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) 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 a98596d227..e642813e3b 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 @@ -113,7 +113,11 @@ class RustMatrixClient( private val innerRoomListService = syncService.roomListService() private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope) - private val verificationService = RustSessionVerificationService(rustSyncService, sessionCoroutineScope) + private val verificationService = RustSessionVerificationService( + client = client, + syncService = rustSyncService, + sessionCoroutineScope = sessionCoroutineScope, + ).apply { start() } private val pushersService = RustPushersService( client = client, dispatchers = dispatchers, 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 3872d27ab5..157e751d21 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.sync.SyncState import io.element.android.libraries.matrix.api.verification.SessionVerificationData @@ -24,22 +25,31 @@ 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.sync.RustSyncService +import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch +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.SessionVerificationControllerInterface +import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData class RustSessionVerificationService( + client: Client, private val syncService: RustSyncService, private val sessionCoroutineScope: CoroutineScope, ) : SessionVerificationService, SessionVerificationControllerDelegate { + private var recoveryStateListenerTaskHandle: TaskHandle? = null + private val encryptionService: Encryption = client.encryption() var verificationController: SessionVerificationControllerInterface? = null set(value) { field = value @@ -64,6 +74,16 @@ class RustSessionVerificationService( syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified } + fun start() { + recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener { + override fun onUpdate(status: RecoveryState) { + sessionCoroutineScope.launch { + updateVerificationStatus(verificationController?.isVerified().orFalse()) + } + } + }) + } + override suspend fun requestVerification() = tryOrFail { verificationController?.requestVerification() } @@ -125,6 +145,8 @@ class RustSessionVerificationService( } fun destroy() { + recoveryStateListenerTaskHandle?.cancelAndDestroy() + verificationController?.setDelegate(null) (verificationController as? SessionVerificationController)?.destroy() verificationController = null } From 3f7d4ae0e5176910290aa5189326ba8a73e93daa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 14:38:43 +0100 Subject: [PATCH 17/27] Fix compilation issue on minimal. --- .../kotlin/io/element/android/samples/minimal/RoomListScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 1730ff1e90..a7d38a2036 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -104,7 +104,6 @@ class RoomListScreen( indicatorService = DefaultIndicatorService( sessionVerificationService = sessionVerificationService, encryptionService = encryptionService, - featureFlagService = featureFlagService, ), featureFlagService = featureFlagService, migrationScreenPresenter = MigrationScreenPresenter( From 67190bdd1dec06132215645259cdaee666599438 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 15:21:04 +0100 Subject: [PATCH 18/27] Fix parameter order. --- .../features/verifysession/impl/VerifySelfSessionView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 871825539a..69fdfc5dfc 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 @@ -61,9 +61,9 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver @Composable fun VerifySelfSessionView( state: VerifySelfSessionState, - modifier: Modifier = Modifier, onEnterRecoveryKey: () -> Unit, goBack: () -> Unit, + modifier: Modifier = Modifier, ) { fun goBackAndCancelIfNeeded() { state.eventSink(VerifySelfSessionViewEvents.CancelAndClose) From 849f1021fd660aa2d37e97a1ee1b8bf2d333b273 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 15:56:12 +0100 Subject: [PATCH 19/27] Improve test framework. --- .../android/tests/testutils/EnsureCalledOnce.kt | 2 +- .../SemanticsNodeInteractionsProviderExtensions.kt | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt index b2c943e1ca..2ea4e44c75 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureCalledOnce.kt @@ -29,7 +29,7 @@ class EnsureCalledOnce : () -> Unit { } } -fun ensureCalledOnce(block: (callback: EnsureCalledOnce) -> Unit) { +fun ensureCalledOnce(block: (callback: () -> Unit) -> Unit) { val callback = EnsureCalledOnce() block(callback) callback.assertSuccess() diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt index 7cf00ca88a..1ebbd80acf 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/SemanticsNodeInteractionsProviderExtensions.kt @@ -34,11 +34,21 @@ fun AndroidComposeTestRule.clickOn(@StringR .performClick() } +/** + * Press the back button in the app bar. + */ fun AndroidComposeTestRule.pressBack() { val text = activity.getString(CommonStrings.action_back) onNode(hasContentDescription(text)).performClick() } +/** + * Press the back key. + */ +fun AndroidComposeTestRule.pressBackKey() { + activity.onBackPressedDispatcher.onBackPressed() +} + fun SemanticsNodeInteractionsProvider.pressTag(tag: String) { onNode(hasTestTag(tag)).performClick() } From 8605efc3df3c92726a18a14f67e38524f1083437 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 15:57:14 +0100 Subject: [PATCH 20/27] Add tests on VerifySelfSessionView --- features/verifysession/impl/build.gradle.kts | 8 + .../impl/VerifySelfSessionStateProvider.kt | 7 +- .../impl/VerifySelfSessionViewTest.kt | 149 ++++++++++++++++++ 3 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt diff --git a/features/verifysession/impl/build.gradle.kts b/features/verifysession/impl/build.gradle.kts index a85d714d1e..3f62c6898e 100644 --- a/features/verifysession/impl/build.gradle.kts +++ b/features/verifysession/impl/build.gradle.kts @@ -22,6 +22,11 @@ plugins { android { namespace = "io.element.android.features.verifysession.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } anvil { @@ -44,10 +49,13 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) + testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) ksp(libs.showkase.processor) } 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 eeaf5c4e38..59d42f11cd 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 @@ -50,7 +50,7 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider = aVerificationEmojiList(), ): SessionVerificationData { return SessionVerificationData.Emojis(emojiList) @@ -62,11 +62,12 @@ private fun aDecimalsSessionVerificationData( return SessionVerificationData.Decimals(decimals) } -private fun aVerifySelfSessionState( +internal fun aVerifySelfSessionState( verificationFlowStep: VerifySelfSessionState.VerificationStep = VerifySelfSessionState.VerificationStep.Initial(false), + eventSink: (VerifySelfSessionViewEvents) -> Unit = {}, ) = VerifySelfSessionState( verificationFlowStep = verificationFlowStep, - eventSink = {}, + eventSink = eventSink, ) private fun aVerificationEmojiList() = listOf( 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 new file mode 100644 index 0000000000..63f294352b --- /dev/null +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 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.verifysession.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +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.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBackKey +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class VerifySelfSessionViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on cancel calls the expected callback and emits the expected Event`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = callback, + ) + } + rule.clickOn(CommonStrings.action_cancel) + } + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose) + } + + @Test + fun `clicking on back key calls the expected callback and emits the expected Event`() { + val eventsRecorder = EventsRecorder() + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = callback, + ) + } + rule.pressBackKey() + } + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.CancelAndClose) + } + + @Test + fun `when flow is completed, the expected callback is invoked`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed, + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = callback, + ) + } + } + } + + @Test + fun `clicking on enter recovery key calls the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = callback, + goBack = EnsureNeverCalled(), + ) + } + rule.clickOn(R.string.screen_session_verification_enter_recovery_key) + } + } + + @Test + fun `clicking on they match emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = EnsureNeverCalled(), + ) + } + rule.clickOn(R.string.screen_session_verification_they_match) + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.ConfirmVerification) + } + + @Test + fun `clicking on they do not match emits the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setContent { + VerifySelfSessionView( + aVerifySelfSessionState( + verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying( + data = aEmojisSessionVerificationData(), + state = AsyncData.Uninitialized, + ), + eventSink = eventsRecorder + ), + onEnterRecoveryKey = EnsureNeverCalled(), + goBack = EnsureNeverCalled(), + ) + } + rule.clickOn(R.string.screen_session_verification_they_dont_match) + eventsRecorder.assertSingle(VerifySelfSessionViewEvents.DeclineVerification) + } +} From 7d41fbffbcb78555d6761d2dc1b8b2a712fd76d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 16:30:22 +0100 Subject: [PATCH 21/27] Remove empty line. --- .../android/features/verifysession/impl/VerifySelfSessionNode.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt index c7369aab0e..3aa75f0c26 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt @@ -34,7 +34,6 @@ class VerifySelfSessionNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: VerifySelfSessionPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onEnterRecoveryKey() { plugins().forEach { it.onEnterRecoveryKey() From 379cdb22fb94dc1a5e7bb63bdd5b900ad256abd9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 16:52:54 +0100 Subject: [PATCH 22/27] Add tests on RoomListView (e2e banner) --- .../roomlist/impl/RoomListStateProvider.kt | 65 +++++---- .../roomlist/impl/RoomListViewTest.kt | 125 ++++++++++++++++++ 2 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index e35e785e70..89597fea06 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -17,10 +17,12 @@ package io.element.android.features.roomlist.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary +import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.aRoomListSearchState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -37,34 +39,47 @@ open class RoomListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomListState(), - aRoomListState().copy(securityBannerState = SecurityBannerState.SessionVerification), - aRoomListState().copy(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), - aRoomListState().copy(hasNetworkConnection = false), - aRoomListState().copy(invitesState = InvitesState.SeenInvites), - aRoomListState().copy(invitesState = InvitesState.NewInvites), - aRoomListState().copy(contextMenu = aContextMenuShown(roomName = "A nice room name")), - aRoomListState().copy(contextMenu = aContextMenuShown(isFavorite = true)), - aRoomListState().copy(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), - aRoomListState().copy(roomList = AsyncData.Success(persistentListOf())), - aRoomListState().copy(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), - aRoomListState().copy(matrixUser = null, displayMigrationStatus = true), - aRoomListState().copy(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), + aRoomListState(securityBannerState = SecurityBannerState.SessionVerification), + aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)), + aRoomListState(hasNetworkConnection = false), + aRoomListState(invitesState = InvitesState.SeenInvites), + aRoomListState(invitesState = InvitesState.NewInvites), + aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")), + aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)), + aRoomListState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation), + aRoomListState(roomList = AsyncData.Success(persistentListOf())), + aRoomListState(roomList = AsyncData.Loading(prevData = RoomListRoomSummaryFactory.createFakeList())), + aRoomListState(matrixUser = null, displayMigrationStatus = true), + aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")), ) } -internal fun aRoomListState() = RoomListState( - matrixUser = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), - showAvatarIndicator = false, - roomList = AsyncData.Success(aRoomListRoomSummaryList()), - hasNetworkConnection = true, - snackbarMessage = null, - securityBannerState = SecurityBannerState.None, - invitesState = InvitesState.NoInvites, - contextMenu = RoomListState.ContextMenu.Hidden, - leaveRoomState = aLeaveRoomState(), - searchState = aRoomListSearchState(), - displayMigrationStatus = false, - eventSink = {} +internal fun aRoomListState( + matrixUser: MatrixUser? = MatrixUser(userId = UserId("@id:domain"), displayName = "User#1"), + showAvatarIndicator: Boolean = false, + roomList: AsyncData> = AsyncData.Success(aRoomListRoomSummaryList()), + hasNetworkConnection: Boolean = true, + snackbarMessage: SnackbarMessage? = null, + securityBannerState: SecurityBannerState = SecurityBannerState.None, + invitesState: InvitesState = InvitesState.NoInvites, + contextMenu: RoomListState.ContextMenu = RoomListState.ContextMenu.Hidden, + leaveRoomState: LeaveRoomState = aLeaveRoomState(), + searchState: RoomListSearchState = aRoomListSearchState(), + displayMigrationStatus: Boolean = false, + eventSink: (RoomListEvents) -> Unit = {} +) = RoomListState( + matrixUser = matrixUser, + showAvatarIndicator = showAvatarIndicator, + roomList = roomList, + hasNetworkConnection = hasNetworkConnection, + snackbarMessage = snackbarMessage, + securityBannerState = securityBannerState, + invitesState = invitesState, + contextMenu = contextMenu, + leaveRoomState = leaveRoomState, + searchState = searchState, + displayMigrationStatus = displayMigrationStatus, + eventSink = eventSink, ) internal fun aRoomListRoomSummaryList(): ImmutableList { diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt new file mode 100644 index 0000000000..c52a8d4614 --- /dev/null +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 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.roomlist.impl + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.roomlist.impl.components.RoomListMenuAction +import io.element.android.libraries.matrix.api.core.RoomId +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 +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RoomListViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on close verification banner emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.SessionVerification, + eventSink = eventsRecorder, + ) + ) + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() + eventsRecorder.assertSingle(RoomListEvents.DismissRequestVerificationPrompt) + } + + @Test + fun `clicking on continue verification banner invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.SessionVerification, + eventSink = eventsRecorder, + ), + onVerifyClicked = callback, + ) + rule.clickOn(CommonStrings.action_continue) + } + } + + @Test + fun `clicking on close recovery key banner emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, + eventSink = eventsRecorder, + ) + ) + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() + eventsRecorder.assertSingle(RoomListEvents.DismissRecoveryKeyPrompt) + } + + @Test + fun `clicking on continue recovery key banner invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = aRoomListState( + securityBannerState = SecurityBannerState.RecoveryKeyConfirmation, + eventSink = eventsRecorder, + ), + onConfirmRecoveryKeyClicked = callback, + ) + rule.clickOn(CommonStrings.action_continue) + } + } +} + +private fun AndroidComposeTestRule.setRoomListView( + state: RoomListState, + onRoomClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onSettingsClicked: () -> Unit = EnsureNeverCalled(), + onVerifyClicked: () -> Unit = EnsureNeverCalled(), + onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(), + onCreateRoomClicked: () -> Unit = EnsureNeverCalled(), + onInvitesClicked: () -> Unit = EnsureNeverCalled(), + onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), +) { + setContent { + RoomListView( + state = state, + onRoomClicked = onRoomClicked, + onSettingsClicked = onSettingsClicked, + onVerifyClicked = onVerifyClicked, + onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, + onCreateRoomClicked = onCreateRoomClicked, + onInvitesClicked = onInvitesClicked, + onRoomSettingsClicked = onRoomSettingsClicked, + onMenuActionClicked = onMenuActionClicked, + ) + } +} From 57900eadbfa2db4b9d70590188a379a7a8ea539b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 17:44:14 +0100 Subject: [PATCH 23/27] Add more tests on RoomListView --- .../roomlist/impl/RoomListViewTest.kt | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index c52a8d4614..347ff65687 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -19,10 +19,14 @@ package io.element.android.features.roomlist.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.longClick import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.roomlist.impl.components.RoomListMenuAction +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -30,6 +34,8 @@ 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 +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -96,6 +102,85 @@ class RoomListViewTest { rule.clickOn(CommonStrings.action_continue) } } + + @Test + fun `clicking on start chat when the session has no room invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = aRoomListState( + eventSink = eventsRecorder, + roomList = AsyncData.Success(persistentListOf()), + ), + onCreateRoomClicked = callback, + ) + rule.clickOn(CommonStrings.action_start_chat) + } + } + + @Test + fun `clicking on a room invokes the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + val state = aRoomListState( + eventSink = eventsRecorder, + ) + val room0 = state.roomList.dataOrNull()!!.first() + ensureCalledOnceWithParam(room0.roomId) { callback -> + rule.setRoomListView( + state = state, + onRoomClicked = callback, + ) + rule.onNodeWithText(room0.lastMessage!!.toString()).performClick() + } + } + + @Test + fun `long clicking on a room emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + eventSink = eventsRecorder, + ) + val room0 = state.roomList.dataOrNull()!!.first() + rule.setRoomListView( + state = state, + ) + rule.onNodeWithText(room0.lastMessage!!.toString()).performTouchInput { longClick() } + eventsRecorder.assertSingle(RoomListEvents.ShowContextMenu(room0)) + } + + @Test + fun `clicking on a room setting invokes the expected callback and emits expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + contextMenu = aContextMenuShown(), + eventSink = eventsRecorder, + ) + val room0 = (state.contextMenu as RoomListState.ContextMenu.Shown).roomId + ensureCalledOnceWithParam(room0) { callback -> + rule.setRoomListView( + state = state, + onRoomSettingsClicked = callback, + ) + rule.clickOn(CommonStrings.common_settings) + } + eventsRecorder.assertSingle(RoomListEvents.HideContextMenu) + } + + @Test + fun `clicking on invites invokes the expected callback`() { + val eventsRecorder = EventsRecorder() + val state = aRoomListState( + invitesState = InvitesState.NewInvites, + eventSink = eventsRecorder, + ) + ensureCalledOnce { callback -> + rule.setRoomListView( + state = state, + onInvitesClicked = callback, + ) + rule.clickOn(CommonStrings.action_invites_list) + } + } } private fun AndroidComposeTestRule.setRoomListView( From 1c4e02797b2a104ab6eec968c6f89bc292062158 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 18:12:40 +0100 Subject: [PATCH 24/27] Fix test `clicking on enter recovery key calls the expected callback` --- .../features/verifysession/impl/VerifySelfSessionViewTest.kt | 2 ++ 1 file changed, 2 insertions(+) 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 63f294352b..4dfad8c9c9 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 @@ -29,6 +29,7 @@ import io.element.android.tests.testutils.pressBackKey import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) class VerifySelfSessionViewTest { @@ -89,6 +90,7 @@ class VerifySelfSessionViewTest { } } + @Config(qualifiers = "h1024dp") @Test fun `clicking on enter recovery key calls the expected callback`() { val eventsRecorder = EventsRecorder(expectEvents = false) From d3d76ddd764e8139cd190f898254428e822826d5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 18:34:15 +0100 Subject: [PATCH 25/27] Cleanup after rebase. --- .../features/roomlist/impl/RoomListView.kt | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 5ca8085fae..290ea0f6a5 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -228,25 +228,23 @@ private fun RoomListContent( // FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80 contentPadding = PaddingValues(bottom = 80.dp) ) { - if (state.displayEmptyState.not()) { - when (state.securityBannerState) { - SecurityBannerState.SessionVerification -> { - item { - RequestVerificationHeader( - onVerifyClicked = onVerifyClicked, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } - ) - } + when { + state.displayEmptyState -> Unit + state.securityBannerState == SecurityBannerState.SessionVerification -> { + item { + RequestVerificationHeader( + onVerifyClicked = onVerifyClicked, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRequestVerificationPrompt) } + ) } - SecurityBannerState.RecoveryKeyConfirmation -> { - item { - ConfirmRecoveryKeyBanner( - onContinueClicked = onConfirmRecoveryKeyClicked, - onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } - ) - } + } + state.securityBannerState == SecurityBannerState.RecoveryKeyConfirmation -> { + item { + ConfirmRecoveryKeyBanner( + onContinueClicked = onConfirmRecoveryKeyClicked, + onDismissClicked = { state.eventSink(RoomListEvents.DismissRecoveryKeyPrompt) } + ) } - SecurityBannerState.None -> Unit } } From 6548aafd2363ed2432d8f81a8e98fdf8050cde37 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Feb 2024 18:39:21 +0100 Subject: [PATCH 26/27] Changelog --- changelog.d/2421.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2421.bugfix diff --git a/changelog.d/2421.bugfix b/changelog.d/2421.bugfix new file mode 100644 index 0000000000..e5f46b0061 --- /dev/null +++ b/changelog.d/2421.bugfix @@ -0,0 +1 @@ +Add ability to enter a recovery key to verify the session. Also fixes some refresh issues with the verification session state. From 155d52dc17c8af031929307b082911b003f85914 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 08:46:38 +0100 Subject: [PATCH 27/27] Update dependency androidx.compose.compiler:compiler to v1.5.10 (#2427) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9dcb238f1f..49c297ccea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ media3 = "1.2.1" # Compose compose_bom = "2024.02.00" -composecompiler = "1.5.9" +composecompiler = "1.5.10" # Coroutines coroutines = "1.8.0"