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.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()

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -23,11 +23,10 @@ interface EncryptionService {
val backupStateStateFlow: StateFlow<BackupState>
val recoveryStateStateFlow: StateFlow<RecoveryState>
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
val isLastDevice: StateFlow<Boolean>
suspend fun enableBackups(): Result<Unit>
suspend fun isLastDevice(): Result<Boolean>
/**
* 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 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<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() {
service.backupStateListener(object : BackupStateListener {
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 {
service.isLastDevice()
}.mapFailure {

View File

@@ -31,6 +31,7 @@ class FakeEncryptionService : EncryptionService {
override val backupStateStateFlow: MutableStateFlow<BackupState> = MutableStateFlow(BackupState.UNKNOWN)
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN)
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Starting)
override val isLastDevice: MutableStateFlow<Boolean> = MutableStateFlow(false)
private var waitForBackupUploadSteadyStateFlow: Flow<BackupUploadState> = 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<Boolean> = simulateLongTask {
return Result.success(isLastDevice)
fun emitIsLastDevice(isLastDevice: Boolean) {
this.isLastDevice.value = isLastDevice
}
override suspend fun resetRecoveryKey(): Result<String> = simulateLongTask {