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 # 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 # 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. # 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" rustls_platform_verifier = "org.rustls:rustls-platform-verifier:0.2.0"
# Others # Others
@@ -345,7 +345,14 @@ class RustMatrixAuthenticationService(
it.scan( it.scan(
qrCodeData = qrCodeData.rustQrCodeData, qrCodeData = qrCodeData.rustQrCodeData,
progressListener = progressListener, 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 // Ensure that the user is not already logged in with the same account
ensureNotAlreadyLoggedIn(client) 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.linknewdevice.LinkDesktopStep
import io.element.android.libraries.matrix.api.logs.LoggerTags import io.element.android.libraries.matrix.api.logs.LoggerTags
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -53,8 +54,15 @@ class RustLinkDesktopHandler(
} }
} }
} }
) ).use { task ->
// We emit Done in case the progress listener was deallocated before scan() sent the Done 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) _linkDesktopStep.emit(LinkDesktopStep.Done)
} catch (e: QrCodeDecodeException) { } catch (e: QrCodeDecodeException) {
Timber.tag(tag.value).w(e, "Invalid QR code scanned") 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.LinkMobileHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
import io.element.android.libraries.matrix.api.logs.LoggerTags import io.element.android.libraries.matrix.api.logs.LoggerTags
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -48,8 +49,15 @@ class RustLinkMobileHandler(
} }
} }
} }
) ).use { task ->
// We emit Done in case the progress listener was deallocated before generate() sent the Done 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) _linkMobileStep.emit(LinkMobileStep.Done)
} catch (e: HumanQrGrantLoginException.Expired) { } 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. // 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. * Please see LICENSE files in the repository root for full details.
*/ */
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.libraries.matrix.impl.auth package io.element.android.libraries.matrix.impl.auth
import com.google.common.truth.Truth.assertThat 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.libraries.workmanager.test.FakeWorkManagerScheduler
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers 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.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -171,6 +177,39 @@ class RustMatrixAuthenticationServiceTest {
closeResult.assertions().isCalledOnce() 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( private fun aSdkQrCodeLoginData() = SdkQrCodeLoginData(
FakeFfiQrCodeData( FakeFfiQrCodeData(
baseUrlResult = { A_HOMESERVER_URL }, 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.GrantGeneratedQrLoginProgress
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgressListener import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgressListener
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeTask
import org.matrix.rustcomponents.sdk.GrantQrLoginProgress import org.matrix.rustcomponents.sdk.GrantQrLoginProgress
import org.matrix.rustcomponents.sdk.GrantQrLoginProgressListener import org.matrix.rustcomponents.sdk.GrantQrLoginProgressListener
import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.NoHandle
@@ -18,21 +19,28 @@ import org.matrix.rustcomponents.sdk.QrCodeData
class FakeFfiGrantLoginWithQrCodeHandler( class FakeFfiGrantLoginWithQrCodeHandler(
private val generateResult: suspend () -> Unit = {}, private val generateResult: suspend () -> Unit = {},
private val scanResult: suspend (QrCodeData) -> Unit = {}, private val scanResult: suspend (QrCodeData) -> Unit = {},
private val cancelResult: () -> Unit = {},
) : GrantLoginWithQrCodeHandler(NoHandle) { ) : GrantLoginWithQrCodeHandler(NoHandle) {
private var generateProgressListener: GrantGeneratedQrLoginProgressListener? = null private var generateProgressListener: GrantGeneratedQrLoginProgressListener? = null
private var scanProgressListener: GrantQrLoginProgressListener? = null private var scanProgressListener: GrantQrLoginProgressListener? = null
override suspend fun generate(progressListener: GrantGeneratedQrLoginProgressListener) { override fun generate(progressListener: GrantGeneratedQrLoginProgressListener): GrantLoginWithQrCodeTask {
generateProgressListener = progressListener generateProgressListener = progressListener
generateResult() return FakeFfiGrantLoginWithQrCodeTask(
waitResult = generateResult,
cancelResult = cancelResult,
)
} }
fun emitGenerateProgress(progress: GrantGeneratedQrLoginProgress) { fun emitGenerateProgress(progress: GrantGeneratedQrLoginProgress) {
generateProgressListener?.onUpdate(progress) generateProgressListener?.onUpdate(progress)
} }
override suspend fun scan(qrCodeData: QrCodeData, progressListener: GrantQrLoginProgressListener) { override fun scan(qrCodeData: QrCodeData, progressListener: GrantQrLoginProgressListener): GrantLoginWithQrCodeTask {
scanProgressListener = progressListener scanProgressListener = progressListener
scanResult(qrCodeData) return FakeFfiGrantLoginWithQrCodeTask(
waitResult = { scanResult(qrCodeData) },
cancelResult = cancelResult,
)
} }
fun emitScanProgress(progress: GrantQrLoginProgress) { 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.GeneratedQrLoginProgressListener
import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler import org.matrix.rustcomponents.sdk.LoginWithQrCodeHandler
import org.matrix.rustcomponents.sdk.LoginWithQrCodeTask
import org.matrix.rustcomponents.sdk.NoHandle import org.matrix.rustcomponents.sdk.NoHandle
import org.matrix.rustcomponents.sdk.QrCodeData import org.matrix.rustcomponents.sdk.QrCodeData
import org.matrix.rustcomponents.sdk.QrLoginProgress import org.matrix.rustcomponents.sdk.QrLoginProgress
@@ -17,16 +18,23 @@ import org.matrix.rustcomponents.sdk.QrLoginProgressListener
class FakeFfiLoginWithQrCodeHandler( class FakeFfiLoginWithQrCodeHandler(
private val generateResult: suspend () -> Unit = {}, private val generateResult: suspend () -> Unit = {},
private val scanResult: suspend (QrCodeData) -> Unit = {}, private val scanResult: suspend (QrCodeData) -> Unit = {},
private val cancelResult: () -> Unit = {},
) : LoginWithQrCodeHandler(NoHandle) { ) : LoginWithQrCodeHandler(NoHandle) {
private var scanProgressListener: QrLoginProgressListener? = null private var scanProgressListener: QrLoginProgressListener? = null
override suspend fun generate(progressListener: GeneratedQrLoginProgressListener) { override fun generate(progressListener: GeneratedQrLoginProgressListener): LoginWithQrCodeTask {
generateResult() return FakeFfiLoginWithQrCodeTask(
waitResult = generateResult,
cancelResult = cancelResult,
)
} }
override suspend fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener) { override fun scan(qrCodeData: QrCodeData, progressListener: QrLoginProgressListener): LoginWithQrCodeTask {
scanProgressListener = progressListener scanProgressListener = progressListener
scanResult(qrCodeData) return FakeFfiLoginWithQrCodeTask(
waitResult = { scanResult(qrCodeData) },
cancelResult = cancelResult,
)
} }
fun emitScanProgress(progress: QrLoginProgress) { 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.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
import io.element.android.libraries.matrix.test.QR_CODE_DATA import io.element.android.libraries.matrix.test.QR_CODE_DATA
import io.element.android.tests.testutils.ExpectedResult import io.element.android.tests.testutils.ExpectedResult
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.match import io.element.android.tests.testutils.match
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope 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( private fun TestScope.createRustLinkDesktopHandler(
handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(), handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(),
) = RustLinkDesktopHandler( ) = 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.impl.fixtures.fakes.FakeFfiQrCodeData
import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE
import io.element.android.tests.testutils.ExpectedResult import io.element.android.tests.testutils.ExpectedResult
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.match import io.element.android.tests.testutils.match
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope 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( private fun TestScope.createRustLinkMobileHandler(
handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(), handler: FakeFfiGrantLoginWithQrCodeHandler = FakeFfiGrantLoginWithQrCodeHandler(),
) = RustLinkMobileHandler( ) = RustLinkMobileHandler(