Make isLastSession "live"

This commit is contained in:
Benoit Marty 2024-02-21 10:33:08 +01:00 committed by Benoit Marty
parent fb368f058b
commit 06caf35ff4
9 changed files with 32 additions and 33 deletions

View file

@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
@ -54,11 +53,7 @@ class LogoutPresenter @Inject constructor(
} }
.collectAsState(initial = BackupUploadState.Unknown) .collectAsState(initial = BackupUploadState.Unknown)
var isLastDevice by remember { mutableStateOf(false) } val isLastDevice by encryptionService.isLastDevice.collectAsState()
LaunchedEffect(Unit) {
isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false
}
val backupState by encryptionService.backupStateStateFlow.collectAsState() val backupState by encryptionService.backupStateStateFlow.collectAsState()
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()

View file

@ -17,14 +17,12 @@
package io.element.android.features.logout.impl.direct package io.element.android.features.logout.impl.direct
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.logout.api.direct.DirectLogoutEvents import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutPresenter import io.element.android.features.logout.api.direct.DirectLogoutPresenter
@ -58,10 +56,7 @@ class DefaultDirectLogoutPresenter @Inject constructor(
} }
.collectAsState(initial = BackupUploadState.Unknown) .collectAsState(initial = BackupUploadState.Unknown)
var isLastDevice by remember { mutableStateOf(false) } val isLastDevice by encryptionService.isLastDevice.collectAsState()
LaunchedEffect(Unit) {
isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false
}
fun handleEvents(event: DirectLogoutEvents) { fun handleEvents(event: DirectLogoutEvents) {
when (event) { when (event) {

View file

@ -61,13 +61,13 @@ class LogoutPresenterTest {
fun `present - initial state - last session`() = runTest { fun `present - initial state - last session`() = runTest {
val presenter = createLogoutPresenter( val presenter = createLogoutPresenter(
encryptionService = FakeEncryptionService().apply { encryptionService = FakeEncryptionService().apply {
givenIsLastDevice(true) emitIsLastDevice(true)
} }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(3) skipItems(2)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isLastDevice).isTrue() assertThat(initialState.isLastDevice).isTrue()
assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown) assertThat(initialState.backupUploadState).isEqualTo(BackupUploadState.Unknown)

View file

@ -55,13 +55,12 @@ class DefaultDirectLogoutPresenterTest {
fun `present - initial state - last session`() = runTest { fun `present - initial state - last session`() = runTest {
val presenter = createDefaultDirectLogoutPresenter( val presenter = createDefaultDirectLogoutPresenter(
encryptionService = FakeEncryptionService().apply { encryptionService = FakeEncryptionService().apply {
givenIsLastDevice(true) emitIsLastDevice(true)
} }
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.canDoDirectSignOut).isFalse() assertThat(initialState.canDoDirectSignOut).isFalse()
assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized) assertThat(initialState.logoutAction).isEqualTo(AsyncAction.Uninitialized)

View file

@ -113,10 +113,7 @@ class RoomListPresenter @Inject constructor(
var securityBannerDismissed by rememberSaveable { mutableStateOf(false) } var securityBannerDismissed by rememberSaveable { mutableStateOf(false) }
val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false) val canVerifySession by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = false)
var isLastDevice by remember { mutableStateOf(false) } val isLastDevice by encryptionService.isLastDevice.collectAsState()
LaunchedEffect(Unit) {
isLastDevice = encryptionService.isLastDevice().getOrNull() ?: false
}
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val syncState by syncService.syncState.collectAsState() val syncState by syncService.syncState.collectAsState()
val securityBannerState by remember { val securityBannerState by remember {

View file

@ -243,14 +243,14 @@ class RoomListPresenterTests {
coroutineScope = scope, coroutineScope = scope,
client = FakeMatrixClient( client = FakeMatrixClient(
encryptionService = FakeEncryptionService().apply { encryptionService = FakeEncryptionService().apply {
givenIsLastDevice(true) emitIsLastDevice(true)
} }
), ),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(2) skipItems(1)
val eventSink = awaitItem().eventSink val eventSink = awaitItem().eventSink
// For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation // For the last session, the state is not SessionVerification, but RecoveryKeyConfirmation
assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation) assertThat(awaitItem().securityBannerState).isEqualTo(SecurityBannerState.RecoveryKeyConfirmation)

View file

@ -23,11 +23,10 @@ interface EncryptionService {
val backupStateStateFlow: StateFlow<BackupState> val backupStateStateFlow: StateFlow<BackupState>
val recoveryStateStateFlow: StateFlow<RecoveryState> val recoveryStateStateFlow: StateFlow<RecoveryState>
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress> val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
val isLastDevice: StateFlow<Boolean>
suspend fun enableBackups(): Result<Unit> suspend fun enableBackups(): Result<Unit>
suspend fun isLastDevice(): Result<Boolean>
/** /**
* Enable recovery. Observe enableProgressStateFlow to get progress and recovery key. * Enable recovery. Observe enableProgressStateFlow to get progress and recovery key.
*/ */

View file

@ -27,12 +27,17 @@ 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.sync.RustSyncService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.BackupStateListener import org.matrix.rustcomponents.sdk.BackupStateListener
import org.matrix.rustcomponents.sdk.BackupSteadyStateListener import org.matrix.rustcomponents.sdk.BackupSteadyStateListener
@ -88,6 +93,20 @@ internal class RustEncryptionService(
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Starting) override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = 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<Boolean> = flow {
while (currentCoroutineContext().isActive) {
val result = isLastDevice().getOrDefault(false)
emit(result)
delay(5_000)
}
}
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
fun start() { fun start() {
service.backupStateListener(object : BackupStateListener { service.backupStateListener(object : BackupStateListener {
override fun onUpdate(status: RustBackupState) { override fun onUpdate(status: RustBackupState) {
@ -173,7 +192,7 @@ internal class RustEncryptionService(
} }
} }
override suspend fun isLastDevice(): Result<Boolean> = withContext(dispatchers.io) { private suspend fun isLastDevice(): Result<Boolean> = withContext(dispatchers.io) {
runCatching { runCatching {
service.isLastDevice() service.isLastDevice()
}.mapFailure { }.mapFailure {

View file

@ -31,6 +31,7 @@ class FakeEncryptionService : EncryptionService {
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN) override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN) override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN)
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Starting) override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Starting)
override val isLastDevice: MutableStateFlow<Boolean> = MutableStateFlow(false)
private var waitForBackupUploadSteadyStateFlow: Flow<BackupUploadState> = flowOf() private var waitForBackupUploadSteadyStateFlow: Flow<BackupUploadState> = flowOf()
private var recoverFailure: Exception? = null private var recoverFailure: Exception? = null
@ -73,14 +74,8 @@ class FakeEncryptionService : EncryptionService {
return Result.success(Unit) return Result.success(Unit)
} }
private var isLastDevice = false fun emitIsLastDevice(isLastDevice: Boolean) {
this.isLastDevice.value = isLastDevice
fun givenIsLastDevice(isLastDevice: Boolean) {
this.isLastDevice = isLastDevice
}
override suspend fun isLastDevice(): Result<Boolean> = simulateLongTask {
return Result.success(isLastDevice)
} }
override suspend fun resetRecoveryKey(): Result<String> = simulateLongTask { override suspend fun resetRecoveryKey(): Result<String> = simulateLongTask {