Move session verification to FTUE flow, make it mandatory (#2594)

* Move session verification to the FTUE
* Allow session verification flow to be restarted
* Use `EncryptionService` to display session verification faster
* Remove session verification item from settings
* Remove session verification banner from room list
* Remove 'verification needed' variant from the `TimelineEncryptedHistoryBanner`
* Improve verification flow UI and UX
* Remove 'verification successful' snackbar message
* Only register push provider after the session has been verified
* Hide room list while the session hasn't been verified
* Prevent deep links from changing the navigation if the session isn't verified
* Update screenshots
* Renamed `FtueState` to `FtueService`, created an actual `FtueState`.

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-04-03 16:53:17 +02:00 committed by GitHub
parent 05f6770d35
commit 41287c5f59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
198 changed files with 822 additions and 761 deletions

View file

@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryServic
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
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.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -84,8 +85,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -130,11 +130,6 @@ class RustMatrixClient(
private val innerRoomListService = syncService.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
private val verificationService = RustSessionVerificationService(
client = client,
syncService = rustSyncService,
sessionCoroutineScope = sessionCoroutineScope,
).apply { start() }
private val pushersService = RustPushersService(
client = client,
dispatchers = dispatchers,
@ -230,6 +225,12 @@ class RustMatrixClient(
),
)
private val verificationService = RustSessionVerificationService(
client = client,
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
sessionCoroutineScope = sessionCoroutineScope,
)
private val eventFilters = TimelineConfig.excludedEvents
.takeIf { it.isNotEmpty() }
?.let { listStateEventType ->
@ -274,11 +275,6 @@ class RustMatrixClient(
.stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf())
init {
roomListService.state.onEach { state ->
if (state == RoomListService.State.Running) {
setupVerificationControllerIfNeeded()
}
}.launchIn(sessionCoroutineScope)
sessionCoroutineScope.launch {
// Force a refresh of the profile
getUserProfile()
@ -490,6 +486,7 @@ class RustMatrixClient(
ignoreSdkError: Boolean,
): String? {
var result: String? = null
syncService.stop()
withContext(sessionDispatcher) {
if (doRequest) {
try {
@ -525,16 +522,6 @@ class RustMatrixClient(
}
}
private fun setupVerificationControllerIfNeeded() {
if (verificationService.verificationController == null) {
try {
verificationService.verificationController = client.getSessionVerificationController()
} catch (e: Throwable) {
Timber.e(e, "Could not start verification service. Will try again on the next sliding sync update.")
}
}
}
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
private suspend fun File.getCacheSize(

View file

@ -16,92 +16,135 @@
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
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
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 kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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.VerificationState
import org.matrix.rustcomponents.sdk.VerificationStateListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
class RustSessionVerificationService(
client: Client,
private val syncService: RustSyncService,
private val sessionCoroutineScope: CoroutineScope,
isSyncServiceReady: Flow<Boolean>,
sessionCoroutineScope: CoroutineScope,
) : SessionVerificationService, SessionVerificationControllerDelegate {
private var verificationStateListenerTaskHandle: TaskHandle? = null
private var recoveryStateListenerTaskHandle: TaskHandle? = null
private val encryptionService: Encryption = client.encryption()
var verificationController: SessionVerificationControllerInterface? = null
set(value) {
field = value
_isReady.value = value != null
// If status was 'Unknown', move it to either 'Verified' or 'NotVerified'
if (value != null) {
value.setDelegate(this)
sessionCoroutineScope.launch { updateVerificationStatus(value.isVerified()) }
}
}
private lateinit var verificationController: SessionVerificationController
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
override val verificationFlowState = _verificationFlowState.asStateFlow()
private val _isReady = MutableStateFlow(false)
override val isReady = _isReady.asStateFlow()
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus.asStateFlow()
override val canVerifySessionFlow = combine(sessionVerifiedStatus, syncService.syncState) { verificationStatus, syncState ->
syncState == SyncState.Running && verificationStatus == SessionVerifiedStatus.NotVerified
override val isReady = MutableStateFlow(false)
override val canVerifySessionFlow = combine(sessionVerifiedStatus, isReady) { verificationStatus, isReady ->
isReady && verificationStatus == SessionVerifiedStatus.NotVerified
}
fun start() {
recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener {
override fun onUpdate(status: RecoveryState) {
sessionCoroutineScope.launch {
updateVerificationStatus(verificationController?.isVerified().orFalse())
init {
isSyncServiceReady
.onEach { syncServiceReady ->
if (syncServiceReady) {
isReady.value = true
runCatching {
// If the controller was failed to initialize before, we try to get it again
if (!this::verificationController.isInitialized) {
verificationController = client.getSessionVerificationController()
}
}
.onFailure {
isReady.value = false
Timber.e(it, "Failed to get verification controller. Trying again in next sync.")
}
} else {
isReady.value = false
}
}
})
.launchIn(sessionCoroutineScope)
isReady.onEach { isReady ->
if (isReady) {
Timber.d("Starting verification service")
// Setup delegate
verificationController.setDelegate(this)
// Immediate status update
updateVerificationStatus(encryptionService.verificationState())
// Listen for changes in verification status and update accordingly
verificationStateListenerTaskHandle?.cancelAndDestroy()
verificationStateListenerTaskHandle = encryptionService.verificationStateListener(object : VerificationStateListener {
override fun onUpdate(status: VerificationState) {
Timber.d("New verification state: $status")
updateVerificationStatus(status)
}
})
// In case we enter the recovery key instead we check changes in the recovery state, since the listener above won't be triggered
recoveryStateListenerTaskHandle?.cancelAndDestroy()
recoveryStateListenerTaskHandle = encryptionService.recoveryStateListener(object : RecoveryStateListener {
override fun onUpdate(status: RecoveryState) {
Timber.d("New recovery state: $status")
// We could check the `RecoveryState`, but it's easier to just use the verification state directly
updateVerificationStatus(encryptionService.verificationState())
}
})
} else {
Timber.d("Stopping verification service")
if (this::verificationController.isInitialized) {
verificationController.setDelegate(null)
}
}
}
.launchIn(sessionCoroutineScope)
}
override suspend fun requestVerification() = tryOrFail {
verificationController?.requestVerification()
verificationController.requestVerification()
}
override suspend fun cancelVerification() = tryOrFail { verificationController?.cancelVerification() }
override suspend fun cancelVerification() = tryOrFail { verificationController.cancelVerification() }
override suspend fun approveVerification() = tryOrFail { verificationController?.approveVerification() }
override suspend fun approveVerification() = tryOrFail { verificationController.approveVerification() }
override suspend fun declineVerification() = tryOrFail { verificationController?.declineVerification() }
override suspend fun declineVerification() = tryOrFail { verificationController.declineVerification() }
override suspend fun startVerification() = tryOrFail {
verificationController?.startSasVerification()
verificationController.startSasVerification()
}
private suspend fun tryOrFail(block: suspend () -> Unit) {
runCatching {
block()
}.onFailure { didFail() }
}.onFailure {
Timber.e(it, "Failed to verify session")
didFail()
}
}
// region Delegate implementation
@ -116,13 +159,14 @@ class RustSessionVerificationService(
}
override fun didFail() {
Timber.e("Session verification failed with an unknown error")
_verificationFlowState.value = VerificationFlowState.Failed
}
override fun didFinish() {
_verificationFlowState.value = VerificationFlowState.Finished
// Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it always returns false
updateVerificationStatus(isVerified = true)
updateVerificationStatus(VerificationState.VERIFIED)
}
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
@ -139,25 +183,26 @@ class RustSessionVerificationService(
override suspend fun reset() {
if (isReady.value) {
// Cancel any pending verification attempt
tryOrNull { verificationController?.cancelVerification() }
tryOrNull { verificationController.cancelVerification() }
}
_verificationFlowState.value = VerificationFlowState.Initial
}
fun destroy() {
Timber.d("Destroying RustSessionVerificationService")
recoveryStateListenerTaskHandle?.cancelAndDestroy()
verificationController?.setDelegate(null)
(verificationController as? SessionVerificationController)?.destroy()
verificationController = null
if (this::verificationController.isInitialized) {
verificationController.setDelegate(null)
(verificationController as? SessionVerificationController)?.destroy()
}
}
private fun updateVerificationStatus(isVerified: Boolean) {
val newValue = when {
!isReady.value -> SessionVerifiedStatus.Unknown
!isVerified -> SessionVerifiedStatus.NotVerified
else -> SessionVerifiedStatus.Verified
private fun updateVerificationStatus(verificationState: VerificationState) {
_sessionVerifiedStatus.value = when (verificationState) {
VerificationState.UNKNOWN -> SessionVerifiedStatus.Unknown
VerificationState.VERIFIED -> SessionVerifiedStatus.Verified
VerificationState.UNVERIFIED -> SessionVerifiedStatus.NotVerified
}
_sessionVerifiedStatus.value = newValue
}
}