Link new device using QrCode.

This commit is contained in:
Benoit Marty 2025-12-04 10:52:26 +01:00 committed by Benoit Marty
parent 028741d81c
commit fd446e98dd
94 changed files with 4431 additions and 36 deletions

View file

@ -19,6 +19,8 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.notification.NotificationService
@ -195,6 +197,21 @@ interface MatrixClient {
*/
suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit>
/**
* Check if linking a new device using QrCode is supported by the server.
*/
suspend fun canLinkNewDevice(): Result<Boolean>
/**
* Create a handler to link a new mobile device, i.e. a device capable of scanning QrCodes.
*/
fun createLinkMobileHandler(): Result<LinkMobileHandler>
/**
* Create a handler to link a new desktop device, i.e. a device not capable of scanning QrCodes.
*/
fun createLinkDesktopHandler(): Result<LinkDesktopHandler>
suspend fun performDatabaseVacuum(): Result<Unit>
}

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 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.api.linknewdevice
interface CheckCodeSender {
/**
* Validates the given [code]. Returns true if the code is valid, false otherwise.
* This method can be called multiple times to validate different codes.
*/
suspend fun validate(code: UByte): Boolean
/**
* Sends the given [code].
* This method can be called only once.
*/
suspend fun send(code: UByte): Result<Unit>
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 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.api.linknewdevice
sealed class ErrorType(message: String) : Exception(message) {
/**
* The requested device ID is already in use.
*/
class DeviceIdAlreadyInUse(message: String) : ErrorType(message)
/**
* The check code was incorrect.
*/
class InvalidCheckCode(message: String) : ErrorType(message)
/**
* The other client proposed an unsupported protocol.
*/
class UnsupportedProtocol(message: String) : ErrorType(message)
/**
* Secrets backup not set up properly.
*/
class MissingSecretsBackup(message: String) : ErrorType(message)
/**
* The rendezvous session was not found and might have expired.
*/
class NotFound(message: String) : ErrorType(message)
/**
* The device could not be created.
*/
class UnableToCreateDevice(message: String) : ErrorType(message)
/**
* An unknown error has happened.
*/
class Unknown(message: String) : ErrorType(message)
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 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.api.linknewdevice
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeDecodeException
import kotlinx.coroutines.flow.StateFlow
interface LinkDesktopHandler {
val linkDesktopStep: StateFlow<LinkDesktopStep>
suspend fun handleScannedQrCode(data: ByteArray)
}
sealed interface LinkDesktopStep {
data object Uninitialized : LinkDesktopStep
data object Starting : LinkDesktopStep
data class WaitingForAuth(
val verificationUri: String,
) : LinkDesktopStep
data class EstablishingSecureChannel(
val checkCode: UByte,
val checkCodeString: String,
) : LinkDesktopStep
data class InvalidQrCode(
val error: QrCodeDecodeException,
) : LinkDesktopStep
data class Error(
val errorType: ErrorType,
) : LinkDesktopStep
data object SyncingSecrets : LinkDesktopStep
data object Done : LinkDesktopStep
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2025 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.api.linknewdevice
import kotlinx.coroutines.flow.Flow
interface LinkMobileHandler {
val linkMobileStep: Flow<LinkMobileStep>
suspend fun start()
}
sealed interface LinkMobileStep {
data object Uninitialized : LinkMobileStep
data object Starting : LinkMobileStep
data class QrReady(val data: String) : LinkMobileStep
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
data class Error(val errorType: ErrorType) : LinkMobileStep
data object SyncingSecrets : LinkMobileStep
data object Done : LinkMobileStep
}

View file

@ -0,0 +1,14 @@
/*
* Copyright (c) 2025 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.api.logs
import io.element.android.libraries.core.log.logger.LoggerTag
object LoggerTags {
val linkNewDevice = LoggerTag("LinkNewDevice")
}

View file

@ -25,6 +25,8 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.room.BaseRoom
@ -45,6 +47,8 @@ import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.impl.encryption.RustEncryptionService
import io.element.android.libraries.matrix.impl.exception.mapClientException
import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkDesktopHandler
import io.element.android.libraries.matrix.impl.linknewdevice.RustLinkMobileHandler
import io.element.android.libraries.matrix.impl.mapper.map
import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.media.RustMediaPreviewService
@ -726,6 +730,34 @@ class RustMatrixClient(
}
}
override suspend fun canLinkNewDevice(): Result<Boolean> = withContext(sessionDispatcher) {
runCatchingExceptions {
innerClient.isLoginWithQrCodeSupported()
}
}
override fun createLinkMobileHandler(): Result<LinkMobileHandler> {
return runCatchingExceptions {
val handler = innerClient.newGrantLoginWithQrCodeHandler()
RustLinkMobileHandler(
inner = handler,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
)
}
}
override fun createLinkDesktopHandler(): Result<LinkDesktopHandler> {
return runCatchingExceptions {
val handler = innerClient.newGrantLoginWithQrCodeHandler()
RustLinkDesktopHandler(
inner = handler,
sessionCoroutineScope = sessionCoroutineScope,
sessionDispatcher = sessionDispatcher,
)
}
}
override suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
val room = innerClient.getRoom(roomId.value) ?: error("Could not fetch associated room")

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 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.linknewdevice
import io.element.android.libraries.matrix.api.linknewdevice.ErrorType
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
internal fun HumanQrGrantLoginException.map() = when (this) {
is HumanQrGrantLoginException.DeviceIdAlreadyInUse -> ErrorType.DeviceIdAlreadyInUse(message.orEmpty())
is HumanQrGrantLoginException.InvalidCheckCode -> ErrorType.InvalidCheckCode(message.orEmpty())
is HumanQrGrantLoginException.MissingSecretsBackup -> ErrorType.MissingSecretsBackup(message.orEmpty())
is HumanQrGrantLoginException.NotFound -> ErrorType.NotFound(message.orEmpty())
is HumanQrGrantLoginException.UnableToCreateDevice -> ErrorType.UnableToCreateDevice(message.orEmpty())
is HumanQrGrantLoginException.Unknown -> ErrorType.Unknown(message.orEmpty())
is HumanQrGrantLoginException.UnsupportedProtocol -> ErrorType.UnsupportedProtocol(message.orEmpty())
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 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.linknewdevice
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.linknewdevice.CheckCodeSender
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.CheckCodeSender as FfiCheckCodeSender
class RustCheckCodeSender(
private val inner: FfiCheckCodeSender,
private val sessionDispatcher: CoroutineDispatcher,
) : CheckCodeSender {
override suspend fun validate(code: UByte): Boolean = withContext(sessionDispatcher) {
runCatchingExceptions {
// TODO https://github.com/matrix-org/matrix-rust-sdk/pull/5957
// inner.validate(code)
true
}.getOrNull() ?: true
}
override suspend fun send(code: UByte): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
inner.send(code)
}
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2025 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.linknewdevice
import io.element.android.libraries.core.log.logger.LoggerTag
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.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler
import org.matrix.rustcomponents.sdk.GrantQrLoginProgress
import org.matrix.rustcomponents.sdk.GrantQrLoginProgressListener
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
import org.matrix.rustcomponents.sdk.QrCodeData
import org.matrix.rustcomponents.sdk.QrCodeDecodeException
import timber.log.Timber
private val tag = LoggerTag("RustLinkDesktopHandler", LoggerTags.linkNewDevice)
class RustLinkDesktopHandler(
private val inner: GrantLoginWithQrCodeHandler,
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
) : LinkDesktopHandler {
private val _linkDesktopStep = MutableStateFlow<LinkDesktopStep>(LinkDesktopStep.Uninitialized)
override val linkDesktopStep: StateFlow<LinkDesktopStep> = _linkDesktopStep.asStateFlow()
override suspend fun handleScannedQrCode(data: ByteArray) = withContext(sessionDispatcher) {
Timber.tag(tag.value).d("Emit Uninitialized")
_linkDesktopStep.emit(LinkDesktopStep.Uninitialized)
try {
val qrCodeData = QrCodeData.fromBytes(data)
inner.scan(
qrCodeData = qrCodeData,
progressListener = object : GrantQrLoginProgressListener {
override fun onUpdate(state: GrantQrLoginProgress) {
sessionCoroutineScope.launch {
val mappedState = state.map()
Timber.tag(tag.value).d("Emit ${mappedState::class.java.simpleName}")
_linkDesktopStep.emit(mappedState)
}
}
}
)
} catch (e: QrCodeDecodeException) {
Timber.tag(tag.value).w(e, "Invalid QR code scanned")
_linkDesktopStep.emit(
LinkDesktopStep.InvalidQrCode(
error = QrErrorMapper.map(e)
)
)
} catch (e: HumanQrGrantLoginException) {
Timber.tag(tag.value).w(e, "Error during QR login grant")
_linkDesktopStep.emit(LinkDesktopStep.Error(e.map()))
}
}
private fun GrantQrLoginProgress.map() = when (this) {
GrantQrLoginProgress.Done -> LinkDesktopStep.Done
GrantQrLoginProgress.Starting -> LinkDesktopStep.Starting
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
verificationUri = verificationUri,
)
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
checkCode = checkCode,
checkCodeString = checkCodeString,
)
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 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.linknewdevice
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.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgress
import org.matrix.rustcomponents.sdk.GrantGeneratedQrLoginProgressListener
import org.matrix.rustcomponents.sdk.GrantLoginWithQrCodeHandler
import org.matrix.rustcomponents.sdk.HumanQrGrantLoginException
import timber.log.Timber
private val tag = LoggerTag("RustLinkMobileHandler", LoggerTags.linkNewDevice)
class RustLinkMobileHandler(
private val inner: GrantLoginWithQrCodeHandler,
private val sessionCoroutineScope: CoroutineScope,
private val sessionDispatcher: CoroutineDispatcher,
) : LinkMobileHandler {
private val _linkMobileStep = MutableStateFlow<LinkMobileStep>(LinkMobileStep.Uninitialized)
override val linkMobileStep: Flow<LinkMobileStep> = _linkMobileStep.asStateFlow()
override suspend fun start() = withContext(sessionDispatcher) {
Timber.tag(tag.value).d("Emit Uninitialized")
_linkMobileStep.emit(LinkMobileStep.Uninitialized)
try {
inner.generate(
progressListener = object : GrantGeneratedQrLoginProgressListener {
override fun onUpdate(state: GrantGeneratedQrLoginProgress) {
sessionCoroutineScope.launch {
val mappedState = state.map()
Timber.tag(tag.value).d("Emit ${mappedState::class.java.simpleName}")
_linkMobileStep.emit(mappedState)
}
}
}
)
} catch (e: HumanQrGrantLoginException) {
Timber.tag(tag.value).w(e, "Error during QR login grant")
_linkMobileStep.emit(LinkMobileStep.Error(e.map()))
}
}
private fun GrantGeneratedQrLoginProgress.map(): LinkMobileStep {
return when (this) {
GrantGeneratedQrLoginProgress.Done -> LinkMobileStep.Done
is GrantGeneratedQrLoginProgress.QrReady -> {
LinkMobileStep.QrReady(String(qrCode.toBytes(), Charsets.ISO_8859_1))
}
is GrantGeneratedQrLoginProgress.QrScanned -> LinkMobileStep.QrScanned(
RustCheckCodeSender(
inner = checkCodeSender,
sessionDispatcher = sessionDispatcher,
)
)
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(verificationUri)
}
}
}

View file

@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaPreviewService
import io.element.android.libraries.matrix.api.notification.NotificationService
@ -95,6 +97,9 @@ class FakeMatrixClient(
private val deactivateAccountResult: (String, Boolean) -> Result<Unit> = { _, _ -> lambdaError() },
private val currentSlidingSyncVersionLambda: () -> Result<SlidingSyncVersion> = { lambdaError() },
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private val canLinkNewDeviceResult: () -> Result<Boolean> = { lambdaError() },
private val createLinkMobileHandlerResult: () -> Result<LinkMobileHandler> = { lambdaError() },
private val createLinkDesktopHandlerResult: () -> Result<LinkDesktopHandler> = { lambdaError() },
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
private val canReportRoomLambda: () -> Boolean = { false },
private val isLivekitRtcSupportedLambda: () -> Boolean = { false },
@ -356,4 +361,16 @@ class FakeMatrixClient(
override suspend fun performDatabaseVacuum(): Result<Unit> {
return performDatabaseVacuumLambda()
}
override suspend fun canLinkNewDevice(): Result<Boolean> = simulateLongTask {
return canLinkNewDeviceResult()
}
override fun createLinkDesktopHandler(): Result<LinkDesktopHandler> {
return createLinkDesktopHandlerResult()
}
override fun createLinkMobileHandler(): Result<LinkMobileHandler> {
return createLinkMobileHandlerResult()
}
}

View file

@ -100,3 +100,31 @@ const val A_LOGIN_HINT = "mxid:@alice:example.org"
@ColorInt
const val A_COLOR_INT: Int = 0xFFFF0000.toInt()
// From https://github.com/matrix-org/matrix-rust-sdk/blob/3a63838cdb50cde3d74da920186fbae0a2e6db37/crates/matrix-sdk-crypto/src/types/qr_login.rs#L275
// Test vector for the QR code data, copied from the MSC.
@Suppress("ktlint:standard:argument-list-wrapping")
val QR_CODE_DATA = listOf(
0x4D, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x03, 0xd8, 0x86, 0x68, 0x6a, 0xb2, 0x19, 0x7b,
0x78, 0x0e, 0x30, 0x0a, 0x9d, 0x4a, 0x21, 0x47, 0x48, 0x07, 0x00, 0xd7, 0x92, 0x9f, 0x39,
0xab, 0x31, 0xb9, 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x00, 0x47, 0x68, 0x74, 0x74,
0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73,
0x2e, 0x6c, 0x61, 0x62, 0x2e, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x64, 0x65,
0x76, 0x2f, 0x65, 0x38, 0x64, 0x61, 0x36, 0x33, 0x35, 0x35, 0x2d, 0x35, 0x35, 0x30, 0x62,
0x2d, 0x34, 0x61, 0x33, 0x32, 0x2d, 0x61, 0x31, 0x39, 0x33, 0x2d, 0x31, 0x36, 0x31, 0x39,
0x64, 0x39, 0x38, 0x33, 0x30, 0x36, 0x36, 0x38,
).map { it.toByte() }.toByteArray()
// Test vector for the QR code data, copied from the MSC, with the mode set to reciprocate.
@Suppress("ktlint:standard:argument-list-wrapping")
val QR_CODE_DATA_RECIPROCATE = listOf(
0x4D, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x04, 0xd8, 0x86, 0x68, 0x6a, 0xb2, 0x19, 0x7b,
0x78, 0x0e, 0x30, 0x0a, 0x9d, 0x4a, 0x21, 0x47, 0x48, 0x07, 0x00, 0xd7, 0x92, 0x9f, 0x39,
0xab, 0x31, 0xb9, 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x00, 0x47, 0x68, 0x74, 0x74,
0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73,
0x2e, 0x6c, 0x61, 0x62, 0x2e, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x64, 0x65,
0x76, 0x2f, 0x65, 0x38, 0x64, 0x61, 0x36, 0x33, 0x35, 0x35, 0x2d, 0x35, 0x35, 0x30, 0x62,
0x2d, 0x34, 0x61, 0x33, 0x32, 0x2d, 0x61, 0x31, 0x39, 0x33, 0x2d, 0x31, 0x36, 0x31, 0x39,
0x64, 0x39, 0x38, 0x33, 0x30, 0x36, 0x36, 0x38, 0x00, 0x0A, 0x6d, 0x61, 0x74, 0x72, 0x69,
0x78, 0x2e, 0x6f, 0x72, 0x67,
).map { it.toByte() }.toByteArray()

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 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.test.linknewdevice
import io.element.android.libraries.matrix.api.linknewdevice.CheckCodeSender
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeCheckCodeSender(
private val validateResult: (UByte) -> Boolean = { lambdaError() },
private val sendResult: (UByte) -> Result<Unit> = { lambdaError() },
) : CheckCodeSender {
override suspend fun validate(code: UByte): Boolean = simulateLongTask {
validateResult(code)
}
override suspend fun send(code: UByte): Result<Unit> = simulateLongTask {
sendResult(code)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 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.test.linknewdevice
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeLinkDesktopHandler(
private val handleScannedQrCodeResult: (ByteArray) -> Unit = { lambdaError() },
) : LinkDesktopHandler {
private val mutableLinkDesktopStep: MutableStateFlow<LinkDesktopStep> = MutableStateFlow(LinkDesktopStep.Uninitialized)
override val linkDesktopStep: StateFlow<LinkDesktopStep>
get() = mutableLinkDesktopStep.asStateFlow()
override suspend fun handleScannedQrCode(data: ByteArray) {
handleScannedQrCodeResult(data)
}
suspend fun emitStep(step: LinkDesktopStep) {
mutableLinkDesktopStep.emit(step)
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 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.test.linknewdevice
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileHandler
import io.element.android.libraries.matrix.api.linknewdevice.LinkMobileStep
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeLinkMobileHandler(
private val startResult: () -> Unit = { lambdaError() },
) : LinkMobileHandler {
private val mutableLinkMobileStep: MutableStateFlow<LinkMobileStep> = MutableStateFlow(LinkMobileStep.Uninitialized)
override val linkMobileStep: StateFlow<LinkMobileStep>
get() = mutableLinkMobileStep.asStateFlow()
override suspend fun start() = simulateLongTask {
startResult()
}
suspend fun emitStep(step: LinkMobileStep) {
mutableLinkMobileStep.emit(step)
}
}