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(