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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ElementBot
2026-10-13 10:21:21 +01:00
committed by GitHub
parent 23a79ad987
commit 092cded211
11 changed files with 176 additions and 14 deletions
+1 -1
View File
@@ -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
@@ -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)
@@ -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")
@@ -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.
@@ -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<Unit> {}
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 },
@@ -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) {
@@ -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
}
@@ -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) {
@@ -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
}
@@ -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<Unit> { }
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(
@@ -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<Unit> { }
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(