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 {