From 092cded211e6d37849dc2a9db3a32919e941a90e Mon Sep 17 00:00:00 2001 From: ElementBot <110224175+ElementBot@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:21:21 +0100 Subject: [PATCH] Update Matrix Rust SDK to 26.10.07-nightly (#7880) * Update Matrix Rust SDK to 26.10.07-nightly * Adapt to the new QR code login task API `generate()` and `scan()` of `GrantLoginWithQrCodeHandler` and `LoginWithQrCodeHandler` now return a task instead of suspending until the flow is over. Wait for the task to complete, and cancel it if the coroutine is cancelled, so that the flow is also stopped on the Rust side. Co-Authored-By: Claude Opus 5.5 (1M context) * Add tests for the cancellation of the QR code login tasks Check that cancelling the coroutine of the link new device handlers and of the QR code login cancels the task returned by the SDK, so that the flow is stopped on the Rust side. Co-Authored-By: Claude Opus 5.5 (1M context) * Fix import ordering in the link new device handler tests ktlint expects the imports to be sorted in lexicographic order. Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: Benoit Marty Co-authored-by: Claude Opus 5.5 (1M context) --- gradle/libs.versions.toml | 2 +- .../auth/RustMatrixAuthenticationService.kt | 9 ++++- .../linknewdevice/RustLinkDesktopHandler.kt | 12 +++++- .../linknewdevice/RustLinkMobileHandler.kt | 12 +++++- .../RustMatrixAuthenticationServiceTest.kt | 39 +++++++++++++++++++ .../FakeFfiGrantLoginWithQrCodeHandler.kt | 16 ++++++-- .../fakes/FakeFfiGrantLoginWithQrCodeTask.kt | 20 ++++++++++ .../fakes/FakeFfiLoginWithQrCodeHandler.kt | 16 ++++++-- .../fakes/FakeFfiLoginWithQrCodeTask.kt | 20 ++++++++++ .../RustLinkDesktopHandlerTest.kt | 22 +++++++++++ .../RustLinkMobileHandlerTest.kt | 22 +++++++++++ 11 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeTask.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeTask.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 965aee55c0..9994348e1d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -186,7 +186,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # it as a plain `require`: without this, a library built against a newer SDK would silently drag this app # onto it. With it, this pin always wins, and a library that needs something newer fails at configuration # time naming both versions instead of at runtime with NoSuchMethodError. -matrix_sdk = { module = "org.matrix.rustcomponents:sdk-android", version = { strictly = "26.10.05-nightly" } } +matrix_sdk = { module = "org.matrix.rustcomponents:sdk-android", version = { strictly = "26.10.07-nightly" } } rustls_platform_verifier = "org.rustls:rustls-platform-verifier:0.2.0" # Others diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 90d640e82e..c3644eb959 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -345,7 +345,14 @@ class RustMatrixAuthenticationService( it.scan( qrCodeData = qrCodeData.rustQrCodeData, progressListener = progressListener, - ) + ).use { task -> + try { + task.wait() + } catch (e: CancellationException) { + task.cancel() + throw e + } + } } // Ensure that the user is not already logged in with the same account ensureNotAlreadyLoggedIn(client) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt index 8c9695ae4b..e2b417870e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandler.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep import io.element.android.libraries.matrix.api.logs.LoggerTags import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -53,8 +54,15 @@ class RustLinkDesktopHandler( } } } - ) - // We emit Done in case the progress listener was deallocated before scan() sent the Done + ).use { task -> + try { + task.wait() + } catch (e: CancellationException) { + task.cancel() + throw e + } + } + // We emit Done in case the progress listener was deallocated before the task sent the Done _linkDesktopStep.emit(LinkDesktopStep.Done) } catch (e: QrCodeDecodeException) { Timber.tag(tag.value).w(e, "Invalid QR code scanned") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt index 33f1ad736d..868721ced9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandler.kt @@ -11,6 +11,7 @@ import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep import io.element.android.libraries.matrix.api.logs.LoggerTags +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -48,8 +49,15 @@ class RustLinkMobileHandler( } } } - ) - // We emit Done in case the progress listener was deallocated before generate() sent the Done + ).use { task -> + try { + task.wait() + } catch (e: CancellationException) { + task.cancel() + throw e + } + } + // We emit Done in case the progress listener was deallocated before the task sent the Done _linkMobileStep.emit(LinkMobileStep.Done) } catch (e: HumanQrGrantLoginException.Expired) { // Note: the SDK does not return this error, but is returning `.NotFound` instead when the QR code expires. We catch both just in case. diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index df564ca710..95047ea452 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt @@ -6,6 +6,8 @@ * Please see LICENSE files in the repository root for full details. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat @@ -36,7 +38,11 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -171,6 +177,39 @@ class RustMatrixAuthenticationServiceTest { closeResult.assertions().isCalledOnce() } + @Test + fun `loginWithQrCode cancels the login task when it is cancelled`() = runTest { + val cancelResult = lambdaRecorder {} + val sut = createRustMatrixAuthenticationService( + clientBuilderProvider = FakeClientBuilderProvider( + provideResult = { + FakeFfiClientBuilder( + buildResult = { + FakeFfiClient( + newLoginWithQrCodeHandlerResult = { + FakeFfiLoginWithQrCodeHandler( + scanResult = { awaitCancellation() }, + cancelResult = cancelResult, + ) + }, + closeResult = {}, + ) + }, + ) + }, + ), + ) + + val job = backgroundScope.launch { + sut.loginWithQrCode(aSdkQrCodeLoginData()) {} + } + runCurrent() + cancelResult.assertions().isNeverCalled() + job.cancel() + runCurrent() + cancelResult.assertions().isCalledOnce() + } + private fun aSdkQrCodeLoginData() = SdkQrCodeLoginData( FakeFfiQrCodeData( baseUrlResult = { A_HOMESERVER_URL }, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt index 0899b16325..c205010ba4 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeHandler.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgress import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgressListener import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler +import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeTask import org.matrix.rustcomponents.sdk.GrantQrLoginProgress import org.matrix.rustcomponents.sdk.GrantQrLoginProgressListener import org.matrix.rustcomponents.sdk.NoHandle @@ -18,21 +19,28 @@ import org.matrix.rustcomponents.sdk.QrCodeData class FakeFfiGrantLoginWithQrCodeHandler( private val generateResult: suspend () -> Unit = {}, private val scanResult: suspend (QrCodeData) -> Unit = {}, + private val cancelResult: () -> Unit = {}, ) : GrantLoginWithQrCodeHandler(NoHandle) { private var generateProgressListener: GrantGeneratedQrLoginProgressListener? = null private var scanProgressListener: GrantQrLoginProgressListener? = null - override suspend fun generate(progressListener: GrantGeneratedQrLoginProgressListener) { + override fun generate(progressListener: GrantGeneratedQrLoginProgressListener): GrantLoginWithQrCodeTask { generateProgressListener = progressListener - generateResult() + return FakeFfiGrantLoginWithQrCodeTask( + waitResult = generateResult, + cancelResult = cancelResult, + ) } fun emitGenerateProgress(progress: GrantGeneratedQrLoginProgress) { generateProgressListener?.onUpdate(progress) } - override suspend fun scan(qrCodeData: QrCodeData, progressListener: GrantQrLoginProgressListener) { + override fun scan(qrCodeData: QrCodeData, progressListener: GrantQrLoginProgressListener): GrantLoginWithQrCodeTask { scanProgressListener = progressListener - scanResult(qrCodeData) + return FakeFfiGrantLoginWithQrCodeTask( + waitResult = { scanResult(qrCodeData) }, + cancelResult = cancelResult, + ) } fun emitScanProgress(progress: GrantQrLoginProgress) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeTask.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeTask.kt new file mode 100644 index 0000000000..016267365a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiGrantLoginWithQrCodeTask.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeTask +import org.matrix.rustcomponents.sdk.NoHandle + +class FakeFfiGrantLoginWithQrCodeTask( + private val waitResult: suspend () -> Unit = {}, + private val cancelResult: () -> Unit = {}, +) : GrantLoginWithQrCodeTask(NoHandle) { + override suspend fun wait() = waitResult() + override fun cancel() = cancelResult() + override fun close() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt index 6e6b750643..457b080154 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeHandler.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.GeneratedQrLoginProgressListener import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler +import org.matrix.rustcomponents.sdk.LoginWithQrCodeTask import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.QrCodeData import org.matrix.rustcomponents.sdk.QrLoginProgress @@ -17,16 +18,23 @@ import org.matrix.rustcomponents.sdk.QrLoginProgressListener class FakeFfiLoginWithQrCodeHandler( private val generateResult: suspend () -> Unit = {}, private val scanResult: suspend (QrCodeData) -> Unit = {}, + private val cancelResult: () -> Unit = {}, ) : LoginWithQrCodeHandler(NoHandle) { private var scanProgressListener: QrLoginProgressListener? = null - override suspend fun generate(progressListener: GeneratedQrLoginProgressListener) { - generateResult() + override fun generate(progressListener: GeneratedQrLoginProgressListener): LoginWithQrCodeTask { + return FakeFfiLoginWithQrCodeTask( + waitResult = generateResult, + cancelResult = cancelResult, + ) } - override suspend fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener) { + override fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener): LoginWithQrCodeTask { scanProgressListener = progressListener - scanResult(qrCodeData) + return FakeFfiLoginWithQrCodeTask( + waitResult = { scanResult(qrCodeData) }, + cancelResult = cancelResult, + ) } fun emitScanProgress(progress: QrLoginProgress) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeTask.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeTask.kt new file mode 100644 index 0000000000..c3fe5e930a --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLoginWithQrCodeTask.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.LoginWithQrCodeTask +import org.matrix.rustcomponents.sdk.NoHandle + +class FakeFfiLoginWithQrCodeTask( + private val waitResult: suspend () -> Unit = {}, + private val cancelResult: () -> Unit = {}, +) : LoginWithQrCodeTask(NoHandle) { + override suspend fun wait() = waitResult() + override fun cancel() = cancelResult() + override fun close() = Unit +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt index 8a0a046946..7c8bd09416 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkDesktopHandlerTest.kt @@ -17,9 +17,11 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuati import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler import io.element.android.libraries.matrix.test.QR_CODE_DATA import io.element.android.tests.testutils.ExpectedResult +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.match import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -140,6 +142,26 @@ class RustLinkDesktopHandlerTest { } } + @Test + fun `when handleScannedQrCode is cancelled, the task is cancelled`() = runTest { + val cancelResult = lambdaRecorder { } + val handler = FakeFfiGrantLoginWithQrCodeHandler( + scanResult = { awaitCancellation() }, + cancelResult = cancelResult, + ) + val sut = createRustLinkDesktopHandler( + handler, + ) + val job = backgroundScope.launch { + sut.handleScannedQrCode(QR_CODE_DATA) + } + runCurrent() + cancelResult.assertions().isNeverCalled() + job.cancel() + runCurrent() + cancelResult.assertions().isCalledOnce() + } + private fun TestScope.createRustLinkDesktopHandler( handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(), ) = RustLinkDesktopHandler( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt index f1f568e3ca..912c79c0ac 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/linknewdevice/RustLinkMobileHandlerTest.kt @@ -19,9 +19,11 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLogin import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE import io.element.android.tests.testutils.ExpectedResult +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.match import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -176,6 +178,26 @@ class RustLinkMobileHandlerTest { } } + @Test + fun `when start is cancelled, the task is cancelled`() = runTest { + val cancelResult = lambdaRecorder { } + val handler = FakeFfiGrantLoginWithQrCodeHandler( + generateResult = { awaitCancellation() }, + cancelResult = cancelResult, + ) + val sut = createRustLinkMobileHandler( + handler, + ) + val job = backgroundScope.launch { + sut.start() + } + runCurrent() + cancelResult.assertions().isNeverCalled() + job.cancel() + runCurrent() + cancelResult.assertions().isCalledOnce() + } + private fun TestScope.createRustLinkMobileHandler( handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(), ) = RustLinkMobileHandler(