Remove login with QR code feature.

This commit is contained in:
Benoit Marty 2024-08-26 14:23:07 +02:00
parent e1ccad5270
commit fb10bd530d
51 changed files with 1 additions and 2694 deletions

View file

@ -1,110 +0,0 @@
/*
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous
import org.amshove.kluent.invoking
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeInstanceOf
import org.amshove.kluent.shouldThrow
import org.amshove.kluent.with
import org.junit.Test
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.rendezvous.channels.ECDHRendezvousChannel
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.common.CommonTestHelper
class RendezvousTest : InstrumentedTest {
@Test
fun shouldSuccessfullyBuildChannels() = CommonTestHelper.runCryptoTest(context()) { _, _ ->
val cases = listOf(
// v1:
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"login.reciprocate\"}",
// v2:
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"login.reciprocate\"}",
)
cases.forEach { input ->
Rendezvous.buildChannelFromCode(input).channel shouldBeInstanceOf ECDHRendezvousChannel::class
}
}
@Test
fun shouldFailToBuildChannelAsUnsupportedAlgorithm() {
invoking {
Rendezvous.buildChannelFromCode(
"{\"rendezvous\":{\"algorithm\":\"bad algo\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"login.reciprocate\"}"
)
} shouldThrow RendezvousError::class with {
this.reason shouldBeEqualTo RendezvousFailureReason.UnsupportedAlgorithm
}
}
@Test
fun shouldFailToBuildChannelAsUnsupportedTransport() {
invoking {
Rendezvous.buildChannelFromCode(
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"bad transport\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"login.reciprocate\"}"
)
} shouldThrow RendezvousError::class with {
this.reason shouldBeEqualTo RendezvousFailureReason.UnsupportedTransport
}
}
@Test
fun shouldFailToBuildChannelWithInvalidIntent() {
invoking {
Rendezvous.buildChannelFromCode(
"{\"rendezvous\":{\"algorithm\":\"org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256\"," +
"\"key\":\"aeSGwYTV1IUhikUyCapzC6p2xG5NpJ4Lwj2UgUMlcTk\",\"transport\":" +
"{\"type\":\"org.matrix.msc3886.http.v1\",\"uri\":\"https://rendezvous.lab.element.dev/bcab62cd-3e34-48b4-bc39-90895da8f6fe\"}}," +
"\"intent\":\"foo\"}"
)
} shouldThrow RendezvousError::class with {
this.reason shouldBeEqualTo RendezvousFailureReason.InvalidCode
}
}
@Test
fun shouldFailToBuildChannelAsInvalidCode() {
val cases = listOf(
"{}",
"rubbish",
""
)
cases.forEach { input ->
invoking {
Rendezvous.buildChannelFromCode(input)
} shouldThrow RendezvousError::class with {
this.reason shouldBeEqualTo RendezvousFailureReason.InvalidCode
}
}
}
}

View file

@ -1,254 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous
import android.net.Uri
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.rendezvous.channels.ECDHRendezvousChannel
import org.matrix.android.sdk.api.rendezvous.model.ECDHRendezvousCode
import org.matrix.android.sdk.api.rendezvous.model.Outcome
import org.matrix.android.sdk.api.rendezvous.model.Payload
import org.matrix.android.sdk.api.rendezvous.model.PayloadType
import org.matrix.android.sdk.api.rendezvous.model.Protocol
import org.matrix.android.sdk.api.rendezvous.model.RendezvousCode
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.api.rendezvous.model.RendezvousIntent
import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportType
import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm
import org.matrix.android.sdk.api.rendezvous.transports.SimpleHttpRendezvousTransport
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.util.MatrixJsonParser
import timber.log.Timber
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO define an end-of-life date for this implementation.
/**
* Implementation of MSC3906 to sign in + E2EE set up using a QR code.
*/
class Rendezvous(
val channel: RendezvousChannel,
val theirIntent: RendezvousIntent,
) {
companion object {
private val TAG = LoggerTag(Rendezvous::class.java.simpleName, LoggerTag.RENDEZVOUS).value
@Throws(RendezvousError::class)
fun buildChannelFromCode(code: String): Rendezvous {
// we first check that the code is valid JSON and has right high-level structure
val genericParsed = try {
// we rely on moshi validating the code and throwing exception if invalid JSON or algorithm doesn't match
MatrixJsonParser.getMoshi().adapter(RendezvousCode::class.java).fromJson(code)
} catch (a: Throwable) {
throw RendezvousError("Malformed code", RendezvousFailureReason.InvalidCode)
} ?: throw RendezvousError("Code is null", RendezvousFailureReason.InvalidCode)
// then we check that algorithm is supported
if (!SecureRendezvousChannelAlgorithm.values().map { it.value }.contains(genericParsed.rendezvous.algorithm)) {
throw RendezvousError("Unsupported algorithm", RendezvousFailureReason.UnsupportedAlgorithm)
}
// and, that the transport is supported
if (!RendezvousTransportType.values().map { it.value }.contains(genericParsed.rendezvous.transport.type)) {
throw RendezvousError("Unsupported transport", RendezvousFailureReason.UnsupportedTransport)
}
// now that we know the overall structure looks sensible, we rely on moshi validating the code and
// throwing exception if other parts are invalid
val supportedParsed = try {
MatrixJsonParser.getMoshi().adapter(ECDHRendezvousCode::class.java).fromJson(code)
} catch (a: Throwable) {
throw RendezvousError("Malformed ECDH rendezvous code", RendezvousFailureReason.InvalidCode)
} ?: throw RendezvousError("ECDH rendezvous code is null", RendezvousFailureReason.InvalidCode)
val transport = SimpleHttpRendezvousTransport(supportedParsed.rendezvous.transport.uri)
return Rendezvous(
ECDHRendezvousChannel(transport, supportedParsed.rendezvous.algorithm, supportedParsed.rendezvous.key),
supportedParsed.intent
)
}
}
private val adapter = MatrixJsonParser.getMoshi().adapter(Payload::class.java)
// not yet implemented: RendezvousIntent.RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
val ourIntent: RendezvousIntent = RendezvousIntent.LOGIN_ON_NEW_DEVICE
@Throws(RendezvousError::class)
private suspend fun checkCompatibility() {
val incompatible = theirIntent == ourIntent
Timber.tag(TAG).d("ourIntent: $ourIntent, theirIntent: $theirIntent, incompatible: $incompatible")
if (incompatible) {
// inform the other side
send(Payload(PayloadType.FINISH, intent = ourIntent))
if (ourIntent == RendezvousIntent.LOGIN_ON_NEW_DEVICE) {
throw RendezvousError("The other device isn't signed in", RendezvousFailureReason.OtherDeviceNotSignedIn)
} else {
throw RendezvousError("The other device is already signed in", RendezvousFailureReason.OtherDeviceAlreadySignedIn)
}
}
}
@Throws(RendezvousError::class)
suspend fun startAfterScanningCode(): String {
val checksum = channel.connect()
Timber.tag(TAG).i("Connected to secure channel with checksum: $checksum")
checkCompatibility()
// get protocols
Timber.tag(TAG).i("Waiting for protocols")
val protocolsResponse = receive()
if (protocolsResponse?.protocols == null || !protocolsResponse.protocols.contains(Protocol.LOGIN_TOKEN)) {
send(Payload(PayloadType.FINISH, outcome = Outcome.UNSUPPORTED))
throw RendezvousError("Unsupported protocols", RendezvousFailureReason.UnsupportedHomeserver)
}
send(Payload(PayloadType.PROGRESS, protocol = Protocol.LOGIN_TOKEN))
return checksum
}
@Throws(RendezvousError::class)
suspend fun waitForLoginOnNewDevice(authenticationService: AuthenticationService): Session {
Timber.tag(TAG).i("Waiting for login_token")
val loginToken = receive()
if (loginToken?.type == PayloadType.FINISH) {
when (loginToken.outcome) {
Outcome.DECLINED -> {
throw RendezvousError("Login declined by other device", RendezvousFailureReason.UserDeclined)
}
Outcome.UNSUPPORTED -> {
throw RendezvousError("Homeserver lacks support", RendezvousFailureReason.UnsupportedHomeserver)
}
else -> {
throw RendezvousError("Unknown error", RendezvousFailureReason.Unknown)
}
}
}
val homeserver = loginToken?.homeserver ?: throw RendezvousError("No homeserver returned", RendezvousFailureReason.ProtocolError)
val token = loginToken.loginToken ?: throw RendezvousError("No login token returned", RendezvousFailureReason.ProtocolError)
Timber.tag(TAG).i("Got login_token now attempting to sign in with $homeserver")
val hsConfig = HomeServerConnectionConfig(homeServerUri = Uri.parse(homeserver))
return authenticationService.loginUsingQrLoginToken(hsConfig, token)
}
@Throws(RendezvousError::class)
suspend fun completeVerificationOnNewDevice(session: Session) {
val userId = session.myUserId
val crypto = session.cryptoService()
val deviceId = crypto.getMyCryptoDevice().deviceId
val deviceKey = crypto.getMyCryptoDevice().fingerprint()
send(Payload(PayloadType.PROGRESS, outcome = Outcome.SUCCESS, deviceId = deviceId, deviceKey = deviceKey))
try {
// explicitly download keys for ourself rather than racing with initial sync which might not complete in time
crypto.downloadKeysIfNeeded(listOf(userId), false)
} catch (e: Throwable) {
// log as warning and continue as initial sync might still complete
Timber.tag(TAG).w(e, "Failed to download keys for self")
}
// await confirmation of verification
val verificationResponse = receive()
if (verificationResponse?.outcome == Outcome.VERIFIED) {
val verifyingDeviceId = verificationResponse.verifyingDeviceId
?: throw RendezvousError("No verifying device id returned", RendezvousFailureReason.ProtocolError)
val verifyingDeviceFromServer = crypto.getCryptoDeviceInfo(userId, verifyingDeviceId)
if (verifyingDeviceFromServer?.fingerprint() != verificationResponse.verifyingDeviceKey) {
Timber.tag(TAG).w(
"Verifying device $verifyingDeviceId key doesn't match: ${
verifyingDeviceFromServer?.fingerprint()
} vs ${verificationResponse.verifyingDeviceKey})"
)
// inform the other side
send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
throw RendezvousError("Key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
}
verificationResponse.masterKey?.let { masterKeyFromVerifyingDevice ->
// verifying device provided us with a master key, so use it to check integrity
// see what the homeserver told us
val localMasterKey = crypto.crossSigningService().getMyCrossSigningKeys()?.masterKey()
// n.b. if no local master key this is a problem, as well as it not matching
if (localMasterKey?.unpaddedBase64PublicKey != masterKeyFromVerifyingDevice) {
Timber.tag(TAG).w("Master key from verifying device doesn't match: $masterKeyFromVerifyingDevice vs $localMasterKey")
// inform the other side
send(Payload(PayloadType.FINISH, outcome = Outcome.E2EE_SECURITY_ERROR))
throw RendezvousError("Master key from verifying device doesn't match", RendezvousFailureReason.E2EESecurityIssue)
}
// set other device as verified
Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified")
crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)
Timber.tag(TAG).i("Setting master key as trusted")
crypto.crossSigningService().markMyMasterKeyAsTrusted()
} ?: run {
// set other device as verified anyway
Timber.tag(TAG).i("Setting device $verifyingDeviceId as verified")
crypto.setDeviceVerification(DeviceTrustLevel(locallyVerified = true, crossSigningVerified = false), userId, verifyingDeviceId)
Timber.tag(TAG).i("No master key given by verifying device")
}
// request secrets from other sessions.
Timber.tag(TAG).i("Requesting secrets from other sessions")
session.sharedSecretStorageService().requestMissingSecrets()
} else {
Timber.tag(TAG).i("Not doing verification")
}
}
@Throws(RendezvousError::class)
private suspend fun receive(): Payload? {
val data = channel.receive() ?: return null
val payload = try {
adapter.fromJson(data.toString(Charsets.UTF_8))
} catch (e: Exception) {
Timber.tag(TAG).w(e, "Failed to parse payload")
throw RendezvousError("Invalid payload received", RendezvousFailureReason.Unknown)
}
return payload
}
private suspend fun send(payload: Payload) {
channel.send(adapter.toJson(payload).toByteArray(Charsets.UTF_8))
}
suspend fun close() {
channel.close()
}
}

View file

@ -1,51 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
/**
* Representation of a rendezvous channel such as that described by MSC3903.
*/
interface RendezvousChannel {
val transport: RendezvousTransport
/**
* @returns the checksum/confirmation digits to be shown to the user
*/
@Throws(RendezvousError::class)
suspend fun connect(): String
/**
* Send a payload via the channel.
* @param data payload to send
*/
@Throws(RendezvousError::class)
suspend fun send(data: ByteArray)
/**
* Receive a payload from the channel.
* @returns the received payload
*/
@Throws(RendezvousError::class)
suspend fun receive(): ByteArray?
/**
* Closes the channel and cleans up.
*/
suspend fun close()
}

View file

@ -1,32 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous
enum class RendezvousFailureReason(val canRetry: Boolean = true) {
UserDeclined,
OtherDeviceNotSignedIn,
OtherDeviceAlreadySignedIn,
Unknown,
Expired,
UserCancelled,
InvalidCode,
UnsupportedAlgorithm(false),
UnsupportedTransport(false),
UnsupportedHomeserver(false),
ProtocolError,
E2EESecurityIssue(false)
}

View file

@ -1,36 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous
import okhttp3.MediaType
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportDetails
interface RendezvousTransport {
var ready: Boolean
@Throws(RendezvousError::class)
suspend fun details(): RendezvousTransportDetails
@Throws(RendezvousError::class)
suspend fun send(contentType: MediaType, data: ByteArray)
@Throws(RendezvousError::class)
suspend fun receive(): ByteArray?
suspend fun close()
}

View file

@ -1,199 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.channels
import android.util.Base64
import com.squareup.moshi.JsonClass
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.MediaType.Companion.toMediaType
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.rendezvous.RendezvousChannel
import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
import org.matrix.android.sdk.api.rendezvous.RendezvousTransport
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.api.rendezvous.model.SecureRendezvousChannelAlgorithm
import org.matrix.android.sdk.api.util.MatrixJsonParser
import org.matrix.android.sdk.internal.crypto.verification.getDecimalCodeRepresentation
import org.matrix.olm.OlmSAS
import timber.log.Timber
import java.security.SecureRandom
import java.util.LinkedList
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Implements X25519 ECDH key agreement and AES-256-GCM encryption channel as per MSC3903:
* https://github.com/matrix-org/matrix-spec-proposals/pull/3903
*/
class ECDHRendezvousChannel(
override var transport: RendezvousTransport,
private val algorithm: SecureRendezvousChannelAlgorithm,
theirPublicKeyBase64: String?,
) : RendezvousChannel {
companion object {
private const val ALGORITHM_SPEC = "AES/GCM/NoPadding"
private const val KEY_SPEC = "AES"
private val TAG = LoggerTag(ECDHRendezvousChannel::class.java.simpleName, LoggerTag.RENDEZVOUS).value
}
@JsonClass(generateAdapter = true)
internal data class ECDHPayload(
val algorithm: SecureRendezvousChannelAlgorithm? = null,
val key: String? = null,
val ciphertext: String? = null,
val iv: String? = null,
)
private val olmSASMutex = Mutex()
private var olmSAS: OlmSAS?
private val ourPublicKey: ByteArray
private val ecdhAdapter = MatrixJsonParser.getMoshi().adapter(ECDHPayload::class.java)
private var theirPublicKey: ByteArray? = null
private var aesKey: ByteArray? = null
init {
theirPublicKeyBase64?.let {
theirPublicKey = decodeBase64(it)
}
olmSAS = OlmSAS()
ourPublicKey = decodeBase64(olmSAS!!.publicKey)
}
fun encodeBase64(input: ByteArray?): String? {
if (algorithm == SecureRendezvousChannelAlgorithm.ECDH_V2) {
return Base64.encodeToString(input, Base64.NO_WRAP or Base64.NO_PADDING)
}
return Base64.encodeToString(input, Base64.NO_WRAP)
}
fun decodeBase64(input: String?): ByteArray {
// for decoding we aren't concerned about padding
return Base64.decode(input, Base64.NO_WRAP)
}
@Throws(RendezvousError::class)
override suspend fun connect(): String {
val sas = olmSAS ?: throw RendezvousError("Channel closed", RendezvousFailureReason.Unknown)
val isInitiator = theirPublicKey == null
if (isInitiator) {
Timber.tag(TAG).i("Waiting for other device to send their public key")
val res = this.receiveAsPayload() ?: throw RendezvousError("No reply from other device", RendezvousFailureReason.ProtocolError)
if (res.key == null) {
throw RendezvousError(
"Unsupported algorithm: ${res.algorithm}",
RendezvousFailureReason.UnsupportedAlgorithm,
)
}
theirPublicKey = decodeBase64(res.key)
} else {
// send our public key unencrypted
Timber.tag(TAG).i("Sending public key")
send(
ECDHPayload(
algorithm = algorithm,
key = encodeBase64(ourPublicKey)
)
)
}
olmSASMutex.withLock {
sas.setTheirPublicKey(encodeBase64(theirPublicKey))
sas.setTheirPublicKey(encodeBase64(theirPublicKey))
val initiatorKey = encodeBase64(if (isInitiator) ourPublicKey else theirPublicKey)
val recipientKey = encodeBase64(if (isInitiator) theirPublicKey else ourPublicKey)
val aesInfo = "${algorithm.value}|$initiatorKey|$recipientKey"
aesKey = sas.generateShortCode(aesInfo, 32)
val rawChecksum = sas.generateShortCode(aesInfo, 5)
return rawChecksum.getDecimalCodeRepresentation(separator = "-")
}
}
private suspend fun send(payload: ECDHPayload) {
transport.send("application/json".toMediaType(), ecdhAdapter.toJson(payload).toByteArray(Charsets.UTF_8))
}
override suspend fun send(data: ByteArray) {
if (aesKey == null) {
throw IllegalStateException("Shared secret not established")
}
send(encrypt(data))
}
private suspend fun receiveAsPayload(): ECDHPayload? {
transport.receive()?.toString(Charsets.UTF_8)?.let {
return ecdhAdapter.fromJson(it)
} ?: return null
}
override suspend fun receive(): ByteArray? {
if (aesKey == null) {
throw IllegalStateException("Shared secret not established")
}
val payload = receiveAsPayload() ?: return null
return decrypt(payload)
}
override suspend fun close() {
val sas = olmSAS ?: throw IllegalStateException("Channel already closed")
olmSASMutex.withLock {
// this does a double release check already so we don't re-check ourselves
sas.releaseSas()
olmSAS = null
}
transport.close()
}
private fun encrypt(plainText: ByteArray): ECDHPayload {
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
val cipherText = LinkedList<Byte>()
val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC)
val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC)
val ivParameterSpec = IvParameterSpec(iv)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
cipherText.addAll(encryptCipher.update(plainText).toList())
cipherText.addAll(encryptCipher.doFinal().toList())
return ECDHPayload(
ciphertext = encodeBase64(cipherText.toByteArray()),
iv = encodeBase64(iv)
)
}
private fun decrypt(payload: ECDHPayload): ByteArray {
val iv = decodeBase64(payload.iv)
val encryptCipher = Cipher.getInstance(ALGORITHM_SPEC)
val secretKeySpec = SecretKeySpec(aesKey, KEY_SPEC)
val ivParameterSpec = IvParameterSpec(iv)
encryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val plainText = LinkedList<Byte>()
plainText.addAll(encryptCipher.update(decodeBase64(payload.ciphertext)).toList())
plainText.addAll(encryptCipher.doFinal().toList())
return plainText.toByteArray()
}
}

View file

@ -1,26 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ECDHRendezvous(
val transport: SimpleHttpRendezvousTransportDetails,
val algorithm: SecureRendezvousChannelAlgorithm,
val key: String
)

View file

@ -1,25 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ECDHRendezvousCode(
val intent: RendezvousIntent,
val rendezvous: ECDHRendezvous
)

View file

@ -1,38 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class Outcome(val value: String) {
@Json(name = "success")
SUCCESS("success"),
@Json(name = "declined")
DECLINED("declined"),
@Json(name = "unsupported")
UNSUPPORTED("unsupported"),
@Json(name = "verified")
VERIFIED("verified"),
@Json(name = "e2ee_security_error")
E2EE_SECURITY_ERROR("e2ee_security_error")
}

View file

@ -1,36 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class Payload(
val type: PayloadType,
val intent: RendezvousIntent? = null,
val outcome: Outcome? = null,
val protocols: List<Protocol>? = null,
val protocol: Protocol? = null,
val homeserver: String? = null,
@Json(name = "login_token") val loginToken: String? = null,
@Json(name = "device_id") val deviceId: String? = null,
@Json(name = "device_key") val deviceKey: String? = null,
@Json(name = "verifying_device_id") val verifyingDeviceId: String? = null,
@Json(name = "verifying_device_key") val verifyingDeviceKey: String? = null,
@Json(name = "master_key") val masterKey: String? = null
)

View file

@ -1,32 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
internal enum class PayloadType(val value: String) {
@Json(name = "m.login.start")
START("m.login.start"),
@Json(name = "m.login.finish")
FINISH("m.login.finish"),
@Json(name = "m.login.progress")
PROGRESS("m.login.progress")
}

View file

@ -1,26 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class Protocol(val value: String) {
@Json(name = "org.matrix.msc3906.login_token")
LOGIN_TOKEN("org.matrix.msc3906.login_token")
}

View file

@ -1,25 +0,0 @@
/*
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
open class Rendezvous(
val transport: RendezvousTransportDetails,
val algorithm: String,
)

View file

@ -1,25 +0,0 @@
/*
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
open class RendezvousCode(
open val intent: RendezvousIntent,
open val rendezvous: Rendezvous
)

View file

@ -1,21 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
class RendezvousError(val description: String, val reason: RendezvousFailureReason) : Exception(description)

View file

@ -1,26 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class RendezvousIntent {
@Json(name = "login.start") LOGIN_ON_NEW_DEVICE,
@Json(name = "login.reciprocate") RECIPROCATE_LOGIN_ON_EXISTING_DEVICE
}

View file

@ -1,24 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
open class RendezvousTransportDetails(
val type: String
)

View file

@ -1,26 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class RendezvousTransportType(val value: String) {
@Json(name = "org.matrix.msc3886.http.v1")
MSC3886_SIMPLE_HTTP_V1("org.matrix.msc3886.http.v1")
}

View file

@ -1,28 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class SecureRendezvousChannelAlgorithm(val value: String) {
@Json(name = "org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256")
ECDH_V1("org.matrix.msc3903.rendezvous.v1.curve25519-aes-sha256"),
@Json(name = "org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256")
ECDH_V2("org.matrix.msc3903.rendezvous.v2.curve25519-aes-sha256")
}

View file

@ -1,24 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.model
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleHttpRendezvousTransportDetails(
val uri: String
) : RendezvousTransportDetails(type = RendezvousTransportType.MSC3886_SIMPLE_HTTP_V1.name)

View file

@ -1,173 +0,0 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.rendezvous.transports
import kotlinx.coroutines.delay
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
import org.matrix.android.sdk.api.rendezvous.RendezvousTransport
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import org.matrix.android.sdk.api.rendezvous.model.RendezvousTransportDetails
import org.matrix.android.sdk.api.rendezvous.model.SimpleHttpRendezvousTransportDetails
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Implementation of the Simple HTTP transport MSC3886: https://github.com/matrix-org/matrix-spec-proposals/pull/3886
*/
class SimpleHttpRendezvousTransport(rendezvousUri: String?) : RendezvousTransport {
companion object {
private val TAG = LoggerTag(SimpleHttpRendezvousTransport::class.java.simpleName, LoggerTag.RENDEZVOUS).value
}
override var ready = false
private var cancelled = false
private var uri: String?
private var etag: String? = null
private var expiresAt: Date? = null
init {
uri = rendezvousUri
}
override suspend fun details(): RendezvousTransportDetails {
val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
return SimpleHttpRendezvousTransportDetails(uri)
}
@Throws(RendezvousError::class)
override suspend fun send(contentType: MediaType, data: ByteArray) {
if (cancelled) {
throw IllegalStateException("Rendezvous cancelled")
}
val method = if (uri != null) "PUT" else "POST"
val uri = this.uri ?: throw RuntimeException("No rendezvous URI")
val httpClient = okhttp3.OkHttpClient.Builder().build()
val request = Request.Builder()
.url(uri)
.method(method, data.toRequestBody())
.header("content-type", contentType.toString())
etag?.let {
request.header("if-match", it)
}
val response = httpClient.newCall(request.build()).execute()
if (response.code == 404) {
throw get404Error()
}
etag = response.header("etag")
Timber.tag(TAG).i("Sent data to $uri new etag $etag")
if (method == "POST") {
val location = response.header("location") ?: throw RuntimeException("No rendezvous URI found in response")
response.header("expires")?.let {
val format = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US)
expiresAt = format.parse(it)
}
// resolve location header which could be relative or absolute
this.uri = response.request.url.toUri().resolve(location).toString()
ready = true
}
}
@Throws(RendezvousError::class)
override suspend fun receive(): ByteArray? {
if (cancelled) {
throw IllegalStateException("Rendezvous cancelled")
}
val uri = uri ?: throw IllegalStateException("Rendezvous not set up")
val httpClient = okhttp3.OkHttpClient.Builder().build()
while (true) {
Timber.tag(TAG).i("Polling: $uri after etag $etag")
val request = Request.Builder()
.url(uri)
.get()
etag?.let {
request.header("if-none-match", it)
}
val response = httpClient.newCall(request.build()).execute()
try {
// expired
if (response.code == 404) {
throw get404Error()
}
// rely on server expiring the channel rather than checking ourselves
if (response.header("content-type") != "application/json") {
response.header("etag")?.let {
etag = it
}
} else if (response.code == 200) {
response.header("etag")?.let {
etag = it
}
return response.body?.bytes()
}
// sleep for a second before polling again
// we rely on the server expiring the channel rather than checking it ourselves
delay(1000)
} finally {
response.close()
}
}
}
private fun get404Error(): RendezvousError {
if (expiresAt != null && Date() > expiresAt) {
return RendezvousError("Expired", RendezvousFailureReason.Expired)
}
return RendezvousError("Received unexpected 404", RendezvousFailureReason.Unknown)
}
override suspend fun close() {
cancelled = true
ready = false
uri?.let {
try {
val httpClient = okhttp3.OkHttpClient.Builder().build()
val request = Request.Builder()
.url(it)
.delete()
.build()
httpClient.newCall(request).execute()
} catch (e: Throwable) {
Timber.tag(TAG).w(e, "Failed to delete channel")
}
}
}
}

View file

@ -85,21 +85,6 @@ class DebugFeaturesStateFactory @Inject constructor(
key = DebugFeatureKeys.newAppLayoutEnabled, key = DebugFeatureKeys.newAppLayoutEnabled,
factory = VectorFeatures::isNewAppLayoutFeatureEnabled factory = VectorFeatures::isNewAppLayoutFeatureEnabled
), ),
createBooleanFeature(
label = "Enable QR Code Login",
key = DebugFeatureKeys.qrCodeLoginEnabled,
factory = VectorFeatures::isQrCodeLoginEnabled
),
createBooleanFeature(
label = "Allow QR Code Login for all servers",
key = DebugFeatureKeys.qrCodeLoginForAllServers,
factory = VectorFeatures::isQrCodeLoginForAllServers
),
createBooleanFeature(
label = "Show QR Code Login in Device Manager",
key = DebugFeatureKeys.reciprocateQrCodeLogin,
factory = VectorFeatures::isReciprocateQrCodeLogin
),
createBooleanFeature( createBooleanFeature(
label = "Enable Voice Broadcast", label = "Enable Voice Broadcast",
key = DebugFeatureKeys.voiceBroadcastEnabled, key = DebugFeatureKeys.voiceBroadcastEnabled,

View file

@ -76,15 +76,6 @@ class DebugVectorFeatures(
override fun isNewAppLayoutFeatureEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled) override fun isNewAppLayoutFeatureEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled)
?: vectorFeatures.isNewAppLayoutFeatureEnabled() ?: vectorFeatures.isNewAppLayoutFeatureEnabled()
override fun isQrCodeLoginEnabled() = read(DebugFeatureKeys.qrCodeLoginEnabled)
?: vectorFeatures.isQrCodeLoginEnabled()
override fun isQrCodeLoginForAllServers() = read(DebugFeatureKeys.qrCodeLoginForAllServers)
?: vectorFeatures.isQrCodeLoginForAllServers()
override fun isReciprocateQrCodeLogin() = read(DebugFeatureKeys.reciprocateQrCodeLogin)
?: vectorFeatures.isReciprocateQrCodeLogin()
override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled)
?: vectorFeatures.isVoiceBroadcastEnabled() ?: vectorFeatures.isVoiceBroadcastEnabled()
@ -150,9 +141,6 @@ object DebugFeatureKeys {
val screenSharing = booleanPreferencesKey("screen-sharing") val screenSharing = booleanPreferencesKey("screen-sharing")
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder") val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
val newAppLayoutEnabled = booleanPreferencesKey("new-app-layout-enabled") val newAppLayoutEnabled = booleanPreferencesKey("new-app-layout-enabled")
val qrCodeLoginEnabled = booleanPreferencesKey("qr-code-login-enabled")
val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers")
val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login")
val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled")
val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled") val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled")
} }

View file

@ -349,7 +349,6 @@
<activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" /> <activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" />
<activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity" /> <activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity" />
<activity android:name=".features.settings.devices.v2.rename.RenameSessionActivity" /> <activity android:name=".features.settings.devices.v2.rename.RenameSessionActivity" />
<activity android:name=".features.login.qr.QrCodeLoginActivity" />
<activity android:name=".features.roomprofile.polls.detail.ui.RoomPollDetailActivity" /> <activity android:name=".features.roomprofile.polls.detail.ui.RoomPollDetailActivity" />
<!-- Services --> <!-- Services -->

View file

@ -61,7 +61,6 @@ import im.vector.app.features.location.LocationSharingViewModel
import im.vector.app.features.location.live.map.LiveLocationMapViewModel import im.vector.app.features.location.live.map.LiveLocationMapViewModel
import im.vector.app.features.location.preview.LocationPreviewViewModel import im.vector.app.features.location.preview.LocationPreviewViewModel
import im.vector.app.features.login.LoginViewModel import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login.qr.QrCodeLoginViewModel
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
import im.vector.app.features.media.VectorAttachmentViewerViewModel import im.vector.app.features.media.VectorAttachmentViewerViewModel
import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.onboarding.OnboardingViewModel
@ -668,11 +667,6 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(RenameSessionViewModel::class) @MavericksViewModelKey(RenameSessionViewModel::class)
fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(QrCodeLoginViewModel::class)
fun qrCodeLoginViewModelFactory(factory: QrCodeLoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(SessionLearnMoreViewModel::class) @MavericksViewModelKey(SessionLearnMoreViewModel::class)

View file

@ -40,9 +40,6 @@ interface VectorFeatures {
* use [VectorPreferences.isNewAppLayoutEnabled] instead. * use [VectorPreferences.isNewAppLayoutEnabled] instead.
*/ */
fun isNewAppLayoutFeatureEnabled(): Boolean fun isNewAppLayoutFeatureEnabled(): Boolean
fun isQrCodeLoginEnabled(): Boolean
fun isQrCodeLoginForAllServers(): Boolean
fun isReciprocateQrCodeLogin(): Boolean
fun isVoiceBroadcastEnabled(): Boolean fun isVoiceBroadcastEnabled(): Boolean
fun isUnverifiedSessionsAlertEnabled(): Boolean fun isUnverifiedSessionsAlertEnabled(): Boolean
} }
@ -60,9 +57,6 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING
override fun forceUsageOfOpusEncoder(): Boolean = false override fun forceUsageOfOpusEncoder(): Boolean = false
override fun isNewAppLayoutFeatureEnabled(): Boolean = true override fun isNewAppLayoutFeatureEnabled(): Boolean = true
override fun isQrCodeLoginEnabled(): Boolean = true
override fun isQrCodeLoginForAllServers(): Boolean = false
override fun isReciprocateQrCodeLogin(): Boolean = false
override fun isVoiceBroadcastEnabled(): Boolean = true override fun isVoiceBroadcastEnabled(): Boolean = true
override fun isUnverifiedSessionsAlertEnabled(): Boolean = true override fun isUnverifiedSessionsAlertEnabled(): Boolean = true
} }

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import im.vector.app.core.platform.VectorViewModelAction
sealed class QrCodeLoginAction : VectorViewModelAction {
data class OnQrCodeScanned(val qrCode: String) : QrCodeLoginAction()
object GenerateQrCode : QrCodeLoginAction()
object ShowQrCode : QrCodeLoginAction()
object TryAgain : QrCodeLoginAction()
}

View file

@ -1,130 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.features.home.HomeActivity
import im.vector.lib.core.utils.compat.getParcelableCompat
import timber.log.Timber
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
// However, we want to keep this implementation around for some time.
// TODO define an end-of-life date for this implementation.
@AndroidEntryPoint
class QrCodeLoginActivity : SimpleFragmentActivity() {
private val viewModel: QrCodeLoginViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
if (isFirstCreation()) {
navigateToInitialFragment()
}
observeViewEvents()
}
private fun navigateToInitialFragment() {
val qrCodeLoginArgs: QrCodeLoginArgs? = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG)
when (qrCodeLoginArgs?.loginType) {
QrCodeLoginType.LOGIN -> {
showInstructionsFragment(qrCodeLoginArgs)
}
QrCodeLoginType.LINK_A_DEVICE -> {
if (qrCodeLoginArgs.showQrCodeImmediately) {
handleNavigateToShowQrCodeScreen()
} else {
showInstructionsFragment(qrCodeLoginArgs)
}
}
null -> {
Timber.i("QrCodeLoginArgs is null. This is not expected.")
finish()
}
}
}
private fun showInstructionsFragment(qrCodeLoginArgs: QrCodeLoginArgs) {
replaceFragment(
views.container,
QrCodeLoginInstructionsFragment::class.java,
qrCodeLoginArgs,
tag = FRAGMENT_QR_CODE_INSTRUCTIONS_TAG
)
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
QrCodeLoginViewEvents.NavigateToStatusScreen -> handleNavigateToStatusScreen()
QrCodeLoginViewEvents.NavigateToShowQrCodeScreen -> handleNavigateToShowQrCodeScreen()
QrCodeLoginViewEvents.NavigateToHomeScreen -> handleNavigateToHomeScreen()
QrCodeLoginViewEvents.NavigateToInitialScreen -> handleNavigateToInitialScreen()
}
}
}
private fun handleNavigateToInitialScreen() {
navigateToInitialFragment()
}
private fun handleNavigateToShowQrCodeScreen() {
addFragment(
views.container,
QrCodeLoginShowQrCodeFragment::class.java,
tag = FRAGMENT_SHOW_QR_CODE_TAG
)
}
private fun handleNavigateToStatusScreen() {
addFragment(
views.container,
QrCodeLoginStatusFragment::class.java,
tag = FRAGMENT_QR_CODE_STATUS_TAG
)
}
private fun handleNavigateToHomeScreen() {
val intent = HomeActivity.newIntent(this, firstStartMainActivity = false, existingSession = true)
startActivity(intent)
}
companion object {
private const val FRAGMENT_QR_CODE_INSTRUCTIONS_TAG = "FRAGMENT_QR_CODE_INSTRUCTIONS_TAG"
private const val FRAGMENT_SHOW_QR_CODE_TAG = "FRAGMENT_SHOW_QR_CODE_TAG"
private const val FRAGMENT_QR_CODE_STATUS_TAG = "FRAGMENT_QR_CODE_STATUS_TAG"
fun getIntent(context: Context, qrCodeLoginArgs: QrCodeLoginArgs): Intent {
return Intent(context, QrCodeLoginActivity::class.java).apply {
putExtra(Mavericks.KEY_ARG, qrCodeLoginArgs)
}
}
}
}

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class QrCodeLoginArgs(
val loginType: QrCodeLoginType,
val showQrCodeImmediately: Boolean,
) : Parcelable

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
sealed class QrCodeLoginConnectionStatus {
object ConnectingToDevice : QrCodeLoginConnectionStatus()
data class Connected(val securityCode: String, val canConfirmSecurityCode: Boolean) : QrCodeLoginConnectionStatus()
object SigningIn : QrCodeLoginConnectionStatus()
data class Failed(val errorType: RendezvousFailureReason, val canTryAgain: Boolean) : QrCodeLoginConnectionStatus()
}

View file

@ -1,81 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.content.Context
import android.content.res.ColorStateList
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.databinding.ViewQrCodeLoginHeaderBinding
class QrCodeLoginHeaderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewQrCodeLoginHeaderBinding.inflate(
LayoutInflater.from(context),
this
)
init {
context.obtainStyledAttributes(
attrs,
im.vector.lib.ui.styles.R.styleable.QrCodeLoginHeaderView,
0,
0
).use {
setTitle(it)
setDescription(it)
setImage(it)
}
}
private fun setTitle(typedArray: TypedArray) {
val title = typedArray.getString(im.vector.lib.ui.styles.R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderTitle)
setTitle(title)
}
private fun setDescription(typedArray: TypedArray) {
val description = typedArray.getString(im.vector.lib.ui.styles.R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderDescription)
setDescription(description)
}
private fun setImage(typedArray: TypedArray) {
val imageResource = typedArray.getResourceId(im.vector.lib.ui.styles.R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderImageResource, 0)
val backgroundTint = typedArray.getColor(im.vector.lib.ui.styles.R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderImageBackgroundTint, 0)
setImage(imageResource, backgroundTint)
}
fun setTitle(title: String?) {
binding.qrCodeLoginHeaderTitleTextView.setTextOrHide(title)
}
fun setDescription(description: String?) {
binding.qrCodeLoginHeaderDescriptionTextView.setTextOrHide(description)
}
fun setImage(imageResource: Int, backgroundTintColor: Int) {
binding.qrCodeLoginHeaderImageView.setImageResource(imageResource)
binding.qrCodeLoginHeaderImageView.backgroundTintList = ColorStateList.valueOf(backgroundTintColor)
}
}

View file

@ -1,105 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginInstructionsBinding
import im.vector.app.features.qrcode.QrCodeScannerActivity
import im.vector.lib.strings.CommonStrings
import timber.log.Timber
@AndroidEntryPoint
class QrCodeLoginInstructionsFragment : VectorBaseFragment<FragmentQrCodeLoginInstructionsBinding>() {
private val viewModel: QrCodeLoginViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeLoginInstructionsBinding {
return FragmentQrCodeLoginInstructionsBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initScanQrCodeButton()
initShowQrCodeButton()
}
private fun initShowQrCodeButton() {
views.qrCodeLoginInstructionsShowQrCodeButton.debouncedClicks {
viewModel.handle(QrCodeLoginAction.ShowQrCode)
}
}
private fun initScanQrCodeButton() {
views.qrCodeLoginInstructionsScanQrCodeButton.debouncedClicks {
QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher)
}
}
private val scanActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
val scannedQrCode = QrCodeScannerActivity.getResultText(activityResult.data)
val wasQrCode = QrCodeScannerActivity.getResultIsQrCode(activityResult.data)
Timber.d("Scanned QR code: $scannedQrCode, was QR code: $wasQrCode")
if (wasQrCode && !scannedQrCode.isNullOrBlank()) {
onQrCodeScanned(scannedQrCode)
} else {
onQrCodeScannerFailed()
}
}
}
private fun onQrCodeScanned(scannedQrCode: String) {
viewModel.handle(QrCodeLoginAction.OnQrCodeScanned(scannedQrCode))
}
private fun onQrCodeScannerFailed() {
// The user scanned something unexpected, so we try scanning again.
// This seems to happen particularly with the large QRs needed for rendezvous
// especially when the QR is partially off the screen
Timber.d("QrCodeLoginInstructionsFragment.onQrCodeScannerFailed - showing scanner again")
QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher)
}
override fun invalidate() = withState(viewModel) { state ->
if (state.loginType == QrCodeLoginType.LOGIN) {
views.qrCodeLoginInstructionsView.setInstructions(
listOf(
getString(CommonStrings.qr_code_login_new_device_instruction_1),
getString(CommonStrings.qr_code_login_new_device_instruction_2),
getString(CommonStrings.qr_code_login_new_device_instruction_3),
)
)
} else {
views.qrCodeLoginInstructionsView.setInstructions(
listOf(
getString(CommonStrings.qr_code_login_link_a_device_scan_qr_code_instruction_1),
getString(CommonStrings.qr_code_login_link_a_device_scan_qr_code_instruction_2),
)
)
}
}
}

View file

@ -1,79 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.use
import androidx.core.view.isVisible
import im.vector.app.databinding.ViewQrCodeLoginInstructionsBinding
class QrCodeLoginInstructionsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val binding = ViewQrCodeLoginInstructionsBinding.inflate(
LayoutInflater.from(context),
this
)
init {
context.obtainStyledAttributes(
attrs,
im.vector.lib.ui.styles.R.styleable.QrCodeLoginInstructionsView,
0,
0
).use {
setInstructions(it)
}
}
private fun setInstructions(typedArray: TypedArray) {
val instruction1 = typedArray.getString(im.vector.lib.ui.styles.R.styleable.QrCodeLoginInstructionsView_qrCodeLoginInstruction1)
val instruction2 = typedArray.getString(im.vector.lib.ui.styles.R.styleable.QrCodeLoginInstructionsView_qrCodeLoginInstruction2)
val instruction3 = typedArray.getString(im.vector.lib.ui.styles.R.styleable.QrCodeLoginInstructionsView_qrCodeLoginInstruction3)
setInstructions(
listOf(
instruction1,
instruction2,
instruction3,
)
)
}
fun setInstructions(instructions: List<String?>?) {
setInstruction(binding.instructions1Layout, binding.instruction1TextView, instructions?.getOrNull(0))
setInstruction(binding.instructions2Layout, binding.instruction2TextView, instructions?.getOrNull(1))
setInstruction(binding.instructions3Layout, binding.instruction3TextView, instructions?.getOrNull(2))
}
private fun setInstruction(instructionLayout: LinearLayout, instructionTextView: TextView, instruction: String?) {
instruction?.let {
instructionLayout.isVisible = true
instructionTextView.text = instruction
} ?: run {
instructionLayout.isVisible = false
}
}
}

View file

@ -1,82 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginShowQrCodeBinding
import im.vector.lib.strings.CommonStrings
@AndroidEntryPoint
class QrCodeLoginShowQrCodeFragment : VectorBaseFragment<FragmentQrCodeLoginShowQrCodeBinding>() {
private val viewModel: QrCodeLoginViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeLoginShowQrCodeBinding {
return FragmentQrCodeLoginShowQrCodeBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initCancelButton()
viewModel.handle(QrCodeLoginAction.GenerateQrCode)
}
private fun initCancelButton() {
views.qrCodeLoginShowQrCodeCancelButton.debouncedClicks {
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
private fun setInstructions(loginType: QrCodeLoginType) {
if (loginType == QrCodeLoginType.LOGIN) {
views.qrCodeLoginShowQrCodeHeaderView.setDescription(getString(CommonStrings.qr_code_login_header_show_qr_code_new_device_description))
views.qrCodeLoginShowQrCodeInstructionsView.setInstructions(
listOf(
getString(CommonStrings.qr_code_login_new_device_instruction_1),
getString(CommonStrings.qr_code_login_new_device_instruction_2),
getString(CommonStrings.qr_code_login_new_device_instruction_3),
)
)
} else {
views.qrCodeLoginShowQrCodeHeaderView.setDescription(getString(CommonStrings.qr_code_login_header_show_qr_code_link_a_device_description))
views.qrCodeLoginShowQrCodeInstructionsView.setInstructions(
listOf(
getString(CommonStrings.qr_code_login_link_a_device_show_qr_code_instruction_1),
getString(CommonStrings.qr_code_login_link_a_device_show_qr_code_instruction_2),
)
)
}
}
private fun showQrCode(qrCodeData: String) {
views.qrCodeLoginSHowQrCodeImageView.setData(qrCodeData)
}
override fun invalidate() = withState(viewModel) { state ->
state.generatedQrCodeData?.let { qrCodeData ->
showQrCode(qrCodeData)
}
setInstructions(state.loginType)
}
}

View file

@ -1,148 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginStatusBinding
import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.strings.CommonStrings
import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
@AndroidEntryPoint
class QrCodeLoginStatusFragment : VectorBaseFragment<FragmentQrCodeLoginStatusBinding>() {
private val viewModel: QrCodeLoginViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeLoginStatusBinding {
return FragmentQrCodeLoginStatusBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initCancelButton()
initTryAgainButton()
}
private fun initTryAgainButton() {
views.qrCodeLoginStatusTryAgainButton.debouncedClicks {
viewModel.handle(QrCodeLoginAction.TryAgain)
}
}
private fun initCancelButton() {
views.qrCodeLoginStatusCancelButton.debouncedClicks {
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
private fun handleFailed(connectionStatus: QrCodeLoginConnectionStatus.Failed) {
views.qrCodeLoginConfirmSecurityCodeLayout.isVisible = false
views.qrCodeLoginStatusLoadingLayout.isVisible = false
views.qrCodeLoginStatusHeaderView.isVisible = true
views.qrCodeLoginStatusSecurityCode.isVisible = false
views.qrCodeLoginStatusNoMatchLayout.isVisible = false
views.qrCodeLoginStatusCancelButton.isVisible = true
views.qrCodeLoginStatusTryAgainButton.isVisible = connectionStatus.canTryAgain
views.qrCodeLoginStatusHeaderView.setTitle(getString(CommonStrings.qr_code_login_header_failed_title))
views.qrCodeLoginStatusHeaderView.setDescription(getErrorDescription(connectionStatus.errorType))
views.qrCodeLoginStatusHeaderView.setImage(
imageResource = R.drawable.ic_qr_code_login_failed,
backgroundTintColor = ThemeUtils.getColor(requireContext(), com.google.android.material.R.attr.colorError)
)
}
private fun getErrorDescription(reason: RendezvousFailureReason): String {
return when (reason) {
RendezvousFailureReason.UnsupportedAlgorithm,
RendezvousFailureReason.UnsupportedTransport -> getString(CommonStrings.qr_code_login_header_failed_device_is_not_supported_description)
RendezvousFailureReason.UnsupportedHomeserver -> getString(CommonStrings.qr_code_login_header_failed_homeserver_is_not_supported_description)
RendezvousFailureReason.Expired -> getString(CommonStrings.qr_code_login_header_failed_timeout_description)
RendezvousFailureReason.UserDeclined -> getString(CommonStrings.qr_code_login_header_failed_denied_description)
RendezvousFailureReason.E2EESecurityIssue -> getString(CommonStrings.qr_code_login_header_failed_e2ee_security_issue_description)
RendezvousFailureReason.OtherDeviceAlreadySignedIn ->
getString(CommonStrings.qr_code_login_header_failed_other_device_already_signed_in_description)
RendezvousFailureReason.OtherDeviceNotSignedIn -> getString(CommonStrings.qr_code_login_header_failed_other_device_not_signed_in_description)
RendezvousFailureReason.InvalidCode -> getString(CommonStrings.qr_code_login_header_failed_invalid_qr_code_description)
RendezvousFailureReason.UserCancelled -> getString(CommonStrings.qr_code_login_header_failed_user_cancelled_description)
else -> getString(CommonStrings.qr_code_login_header_failed_other_description)
}
}
private fun handleConnectingToDevice() {
views.qrCodeLoginConfirmSecurityCodeLayout.isVisible = false
views.qrCodeLoginStatusLoadingLayout.isVisible = true
views.qrCodeLoginStatusHeaderView.isVisible = false
views.qrCodeLoginStatusSecurityCode.isVisible = false
views.qrCodeLoginStatusNoMatchLayout.isVisible = false
views.qrCodeLoginStatusCancelButton.isVisible = true
views.qrCodeLoginStatusTryAgainButton.isVisible = false
views.qrCodeLoginStatusLoadingTextView.setText(CommonStrings.qr_code_login_connecting_to_device)
}
private fun handleSigningIn() {
views.qrCodeLoginConfirmSecurityCodeLayout.isVisible = false
views.qrCodeLoginStatusLoadingLayout.isVisible = true
views.qrCodeLoginStatusHeaderView.apply {
isVisible = true
setTitle(getString(CommonStrings.dialog_title_success))
setDescription("")
setImage(R.drawable.ic_tick, ThemeUtils.getColor(requireContext(), com.google.android.material.R.attr.colorPrimary))
}
views.qrCodeLoginStatusSecurityCode.isVisible = false
views.qrCodeLoginStatusNoMatchLayout.isVisible = false
views.qrCodeLoginStatusCancelButton.isVisible = false
views.qrCodeLoginStatusTryAgainButton.isVisible = false
views.qrCodeLoginStatusLoadingTextView.setText(CommonStrings.qr_code_login_signing_in)
}
private fun handleConnectionEstablished(connectionStatus: QrCodeLoginConnectionStatus.Connected, loginType: QrCodeLoginType) {
views.qrCodeLoginConfirmSecurityCodeLayout.isVisible = loginType == QrCodeLoginType.LINK_A_DEVICE
views.qrCodeLoginStatusLoadingLayout.isVisible = false
views.qrCodeLoginStatusHeaderView.isVisible = true
views.qrCodeLoginStatusSecurityCode.isVisible = true
views.qrCodeLoginStatusNoMatchLayout.isVisible = loginType == QrCodeLoginType.LOGIN
views.qrCodeLoginStatusCancelButton.isVisible = true
views.qrCodeLoginStatusTryAgainButton.isVisible = false
views.qrCodeLoginStatusSecurityCode.text = connectionStatus.securityCode
views.qrCodeLoginStatusHeaderView.setTitle(getString(CommonStrings.qr_code_login_header_connected_title))
views.qrCodeLoginStatusHeaderView.setDescription(getString(CommonStrings.qr_code_login_header_connected_description))
views.qrCodeLoginStatusHeaderView.setImage(
imageResource = R.drawable.ic_qr_code_login_connected,
backgroundTintColor = ThemeUtils.getColor(requireContext(), com.google.android.material.R.attr.colorPrimary)
)
}
override fun invalidate() = withState(viewModel) { state ->
when (state.connectionStatus) {
is QrCodeLoginConnectionStatus.Connected -> handleConnectionEstablished(state.connectionStatus, state.loginType)
QrCodeLoginConnectionStatus.ConnectingToDevice -> handleConnectingToDevice()
QrCodeLoginConnectionStatus.SigningIn -> handleSigningIn()
is QrCodeLoginConnectionStatus.Failed -> handleFailed(state.connectionStatus)
null -> { /* NOOP */
}
}
}
}

View file

@ -1,22 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
enum class QrCodeLoginType {
LOGIN,
LINK_A_DEVICE,
}

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import im.vector.app.core.platform.VectorViewEvents
sealed class QrCodeLoginViewEvents : VectorViewEvents {
object NavigateToStatusScreen : QrCodeLoginViewEvents()
object NavigateToShowQrCodeScreen : QrCodeLoginViewEvents()
object NavigateToHomeScreen : QrCodeLoginViewEvents()
object NavigateToInitialScreen : QrCodeLoginViewEvents()
}

View file

@ -1,166 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.rendezvous.Rendezvous
import org.matrix.android.sdk.api.rendezvous.RendezvousFailureReason
import org.matrix.android.sdk.api.rendezvous.model.RendezvousError
import timber.log.Timber
class QrCodeLoginViewModel @AssistedInject constructor(
@Assisted private val initialState: QrCodeLoginViewState,
private val authenticationService: AuthenticationService,
private val activeSessionHolder: ActiveSessionHolder,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
) : VectorViewModel<QrCodeLoginViewState, QrCodeLoginAction, QrCodeLoginViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<QrCodeLoginViewModel, QrCodeLoginViewState> {
override fun create(initialState: QrCodeLoginViewState): QrCodeLoginViewModel
}
companion object : MavericksViewModelFactory<QrCodeLoginViewModel, QrCodeLoginViewState> by hiltMavericksViewModelFactory() {
val TAG: String = QrCodeLoginViewModel::class.java.simpleName
}
override fun handle(action: QrCodeLoginAction) {
when (action) {
is QrCodeLoginAction.OnQrCodeScanned -> handleOnQrCodeScanned(action)
QrCodeLoginAction.GenerateQrCode -> handleQrCodeViewStarted()
QrCodeLoginAction.ShowQrCode -> handleShowQrCode()
QrCodeLoginAction.TryAgain -> handleTryAgain()
}
}
private fun handleTryAgain() {
setState {
copy(
connectionStatus = null
)
}
_viewEvents.post(QrCodeLoginViewEvents.NavigateToInitialScreen)
}
private fun handleShowQrCode() {
_viewEvents.post(QrCodeLoginViewEvents.NavigateToShowQrCodeScreen)
}
private fun handleQrCodeViewStarted() {
val qrCodeData = generateQrCodeData()
setState {
copy(
generatedQrCodeData = qrCodeData
)
}
}
private fun handleOnQrCodeScanned(action: QrCodeLoginAction.OnQrCodeScanned) {
Timber.tag(TAG).d("Scanned code of length ${action.qrCode.length}")
val rendezvous = try { Rendezvous.buildChannelFromCode(action.qrCode) } catch (t: Throwable) {
Timber.tag(TAG).e(t, "Error occurred during sign in")
if (t is RendezvousError) {
onFailed(t.reason)
} else {
onFailed(RendezvousFailureReason.Unknown)
}
return
}
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.ConnectingToDevice
)
}
_viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
viewModelScope.launch(Dispatchers.IO) {
try {
val confirmationCode = rendezvous.startAfterScanningCode()
Timber.tag(TAG).i("Established secure channel with checksum: $confirmationCode")
onConnectionEstablished(confirmationCode)
val session = rendezvous.waitForLoginOnNewDevice(authenticationService)
onSigningIn()
activeSessionHolder.setActiveSession(session)
authenticationService.reset()
configureAndStartSessionUseCase.execute(session)
rendezvous.completeVerificationOnNewDevice(session)
_viewEvents.post(QrCodeLoginViewEvents.NavigateToHomeScreen)
} catch (t: Throwable) {
Timber.tag(TAG).e(t, "Error occurred during sign in")
if (t is RendezvousError) {
onFailed(t.reason)
} else {
onFailed(RendezvousFailureReason.Unknown)
}
}
}
}
private fun onFailed(reason: RendezvousFailureReason) {
_viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.Failed(reason, reason.canRetry)
)
}
}
private fun onConnectionEstablished(securityCode: String) {
val canConfirmSecurityCode = initialState.loginType == QrCodeLoginType.LINK_A_DEVICE
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.Connected(securityCode, canConfirmSecurityCode)
)
}
}
private fun onSigningIn() {
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.SigningIn
)
}
}
/**
* QR code generation is not currently supported and this is a placeholder for future
* functionality.
*/
private fun generateQrCodeData(): String {
return "NOT SUPPORTED"
}
}

View file

@ -1,30 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import com.airbnb.mvrx.MavericksState
data class QrCodeLoginViewState(
val loginType: QrCodeLoginType,
val connectionStatus: QrCodeLoginConnectionStatus? = null,
val generatedQrCodeData: String? = null,
) : MavericksState {
constructor(args: QrCodeLoginArgs) : this(
loginType = args.loginType,
)
}

View file

@ -71,8 +71,6 @@ import im.vector.app.features.location.live.map.LiveLocationMapViewActivity
import im.vector.app.features.location.live.map.LiveLocationMapViewArgs import im.vector.app.features.location.live.map.LiveLocationMapViewArgs
import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginActivity
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.qr.QrCodeLoginActivity
import im.vector.app.features.login.qr.QrCodeLoginArgs
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
@ -614,14 +612,6 @@ class DefaultNavigator @Inject constructor(
activityResultLauncher.launch(screenCaptureIntent) activityResultLauncher.launch(screenCaptureIntent)
} }
override fun openLoginWithQrCode(context: Context, qrCodeLoginArgs: QrCodeLoginArgs) {
QrCodeLoginActivity
.getIntent(context, qrCodeLoginArgs)
.also {
context.startActivity(it)
}
}
private fun Intent.start(context: Context) { private fun Intent.start(context: Context) {
context.startActivity(this) context.startActivity(this)
} }

View file

@ -31,7 +31,6 @@ import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingMode import im.vector.app.features.location.LocationSharingMode
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.login.qr.QrCodeLoginArgs
import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
import im.vector.app.features.pin.PinMode import im.vector.app.features.pin.PinMode
@ -202,9 +201,4 @@ interface Navigator {
screenCaptureIntent: Intent, screenCaptureIntent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent> activityResultLauncher: ActivityResultLauncher<Intent>
) )
fun openLoginWithQrCode(
context: Context,
qrCodeLoginArgs: QrCodeLoginArgs,
)
} }

View file

@ -121,29 +121,6 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun checkQrCodeLoginCapability() {
if (!vectorFeatures.isQrCodeLoginEnabled()) {
setState {
copy(
canLoginWithQrCode = false
)
}
} else if (vectorFeatures.isQrCodeLoginForAllServers()) {
// allow for all servers
setState {
copy(
canLoginWithQrCode = true
)
}
} else {
setState {
copy(
canLoginWithQrCode = selectedHomeserver.isLoginWithQrSupported
)
}
}
}
private val matrixOrgUrl = stringProvider.getString(im.vector.app.config.R.string.matrix_org_server_url).ensureTrailingSlash() private val matrixOrgUrl = stringProvider.getString(im.vector.app.config.R.string.matrix_org_server_url).ensureTrailingSlash()
private val defaultHomeserverUrl = mdmService.getData(MdmData.DefaultHomeserverUrl, matrixOrgUrl) private val defaultHomeserverUrl = mdmService.getData(MdmData.DefaultHomeserverUrl, matrixOrgUrl)
@ -710,7 +687,6 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else { } else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend { startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend {
checkQrCodeLoginCapability()
postAction() postAction()
}) })
} }

View file

@ -59,8 +59,6 @@ data class OnboardingViewState(
@PersistState @PersistState
val personalizationState: PersonalizationState = PersonalizationState(), val personalizationState: PersonalizationState = PersonalizationState(),
val canLoginWithQrCode: Boolean = false,
) : MavericksState ) : MavericksState
enum class OnboardingFlow { enum class OnboardingFlow {

View file

@ -40,8 +40,6 @@ import im.vector.app.features.VectorFeatures
import im.vector.app.features.login.LoginMode import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SSORedirectRouterActivity
import im.vector.app.features.login.SocialLoginButtonsView import im.vector.app.features.login.SocialLoginButtonsView
import im.vector.app.features.login.qr.QrCodeLoginArgs
import im.vector.app.features.login.qr.QrCodeLoginType
import im.vector.app.features.login.render import im.vector.app.features.login.render
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewEvents
@ -75,26 +73,6 @@ class FtueAuthCombinedLoginFragment :
viewModel.handle(OnboardingAction.UserNameEnteredAction.Login(views.loginInput.content())) viewModel.handle(OnboardingAction.UserNameEnteredAction.Login(views.loginInput.content()))
} }
views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) } views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) }
viewModel.onEach(OnboardingViewState::canLoginWithQrCode) {
configureQrCodeLoginButtonVisibility(it)
}
}
private fun configureQrCodeLoginButtonVisibility(canLoginWithQrCode: Boolean) {
views.loginWithQrCode.isVisible = canLoginWithQrCode
if (canLoginWithQrCode) {
views.loginWithQrCode.debouncedClicks {
navigator
.openLoginWithQrCode(
requireActivity(),
QrCodeLoginArgs(
loginType = QrCodeLoginType.LOGIN,
showQrCodeImmediately = false,
)
)
}
}
} }
private fun setupSubmitButton() { private fun setupSubmitButton() {

View file

@ -41,8 +41,6 @@ import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorFeatures
import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.login.qr.QrCodeLoginArgs
import im.vector.app.features.login.qr.QrCodeLoginType
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
@ -106,7 +104,6 @@ class VectorSettingsDevicesFragment :
initOtherSessionsHeaderView() initOtherSessionsHeaderView()
initOtherSessionsView() initOtherSessionsView()
initSecurityRecommendationsView() initSecurityRecommendationsView()
initQrLoginView()
observeViewEvents() observeViewEvents()
} }
@ -240,38 +237,6 @@ class VectorSettingsDevicesFragment :
} }
} }
private fun initQrLoginView() {
if (!vectorFeatures.isReciprocateQrCodeLogin()) {
views.deviceListHeaderSignInWithQrCode.isVisible = false
views.deviceListHeaderScanQrCodeButton.isVisible = false
views.deviceListHeaderShowQrCodeButton.isVisible = false
return
}
views.deviceListHeaderSignInWithQrCode.isVisible = true
views.deviceListHeaderScanQrCodeButton.isVisible = true
views.deviceListHeaderShowQrCodeButton.isVisible = true
views.deviceListHeaderScanQrCodeButton.debouncedClicks {
navigateToQrCodeScreen(showQrCodeImmediately = false)
}
views.deviceListHeaderShowQrCodeButton.debouncedClicks {
navigateToQrCodeScreen(showQrCodeImmediately = true)
}
}
private fun navigateToQrCodeScreen(showQrCodeImmediately: Boolean) {
navigator
.openLoginWithQrCode(
requireActivity(),
QrCodeLoginArgs(
loginType = QrCodeLoginType.LINK_A_DEVICE,
showQrCodeImmediately = showQrCodeImmediately,
)
)
}
override fun onDestroyView() { override fun onDestroyView() {
cleanUpLearnMoreButtonsListeners() cleanUpLearnMoreButtonsListeners()
super.onDestroyView() super.onDestroyView()

View file

@ -244,20 +244,6 @@
app:layout_constraintStart_toStartOf="@id/loginGutterStart" app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginSubmit" /> app:layout_constraintTop_toBottomOf="@id/loginSubmit" />
<Button
android:id="@+id/loginWithQrCode"
style="@style/Widget.Vector.Button.Outlined.Login"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_marginTop="12dp"
android:text="@string/login_scan_qr_code"
android:visibility="gone"
app:drawableLeftCompat="@drawable/ic_qr_code"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader"
tools:visibility="visible" />
<im.vector.app.features.login.SocialLoginButtonsView <im.vector.app.features.login.SocialLoginButtonsView
android:id="@+id/ssoButtons" android:id="@+id/ssoButtons"
android:layout_width="0dp" android:layout_width="0dp"
@ -266,7 +252,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd" app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart" app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginWithQrCode" app:layout_constraintTop_toBottomOf="@id/ssoButtons"
tools:signMode="signup" /> tools:signMode="signup" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp">
<im.vector.app.features.login.qr.QrCodeLoginHeaderView
android:id="@+id/qrCodeLoginInstructionsHeaderView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:qrCodeLoginHeaderDescription="@string/qr_code_login_header_scan_qr_code_description"
app:qrCodeLoginHeaderImageBackgroundTint="?colorPrimary"
app:qrCodeLoginHeaderImageResource="@drawable/ic_camera"
app:qrCodeLoginHeaderTitle="@string/qr_code_login_header_scan_qr_code_title" />
<im.vector.app.features.login.qr.QrCodeLoginInstructionsView
android:id="@+id/qrCodeLoginInstructionsView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginInstructionsHeaderView"
app:qrCodeLoginInstruction1="@string/qr_code_login_new_device_instruction_1"
app:qrCodeLoginInstruction2="@string/qr_code_login_new_device_instruction_2"
app:qrCodeLoginInstruction3="@string/qr_code_login_new_device_instruction_3" />
<Button
android:id="@+id/qrCodeLoginInstructionsShowQrCodeButton"
style="@style/Widget.Vector.Button.Outlined.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:text="@string/qr_code_login_show_qr_code_button"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<FrameLayout
android:id="@+id/qrCodeLoginInstructionsAlternativeLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/qrCodeLoginInstructionsShowQrCodeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="center_vertical"
android:background="@drawable/divider_horizontal" />
<TextView
android:id="@+id/qrCodeLoginInstructionsAlternativeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?android:colorBackground"
android:paddingHorizontal="12dp"
android:text="@string/qr_code_login_signing_in_a_mobile_device"
app:drawableLeftCompat="@drawable/divider_horizontal"
app:drawableTint="@color/alert_default_error_background" />
</FrameLayout>
<Button
android:id="@+id/qrCodeLoginInstructionsScanQrCodeButton"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="@string/qr_code_login_scan_qr_code_button"
android:textAllCaps="false"
app:layout_constraintBottom_toTopOf="@id/qrCodeLoginInstructionsAlternativeLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:background="?android:colorBackground">
<im.vector.app.features.login.qr.QrCodeLoginHeaderView
android:id="@+id/qrCodeLoginShowQrCodeHeaderView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:qrCodeLoginHeaderDescription="@string/qr_code_login_header_show_qr_code_link_a_device_description"
app:qrCodeLoginHeaderImageBackgroundTint="?colorPrimary"
app:qrCodeLoginHeaderImageResource="@drawable/ic_camera"
app:qrCodeLoginHeaderTitle="@string/qr_code_login_header_show_qr_code_title" />
<im.vector.app.features.login.qr.QrCodeLoginInstructionsView
android:id="@+id/qrCodeLoginShowQrCodeInstructionsView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginShowQrCodeHeaderView"
app:qrCodeLoginInstruction1="@string/qr_code_login_new_device_instruction_1"
app:qrCodeLoginInstruction2="@string/qr_code_login_new_device_instruction_2"
app:qrCodeLoginInstruction3="@string/qr_code_login_new_device_instruction_3" />
<im.vector.app.core.ui.views.QrCodeImageView
android:id="@+id/qrCodeLoginSHowQrCodeImageView"
android:layout_width="240dp"
android:layout_height="240dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginShowQrCodeInstructionsView"
app:layout_constraintBottom_toTopOf="@id/qrCodeLoginShowQrCodeCancelButton"/>
<Button
android:id="@+id/qrCodeLoginShowQrCodeCancelButton"
style="@style/Widget.Vector.Button.Outlined.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:text="@string/action_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,155 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:paddingHorizontal="16dp">
<LinearLayout
android:id="@+id/qrCodeLoginStatusLoadingLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView
android:id="@+id/qrCodeLoginStatusLoadingTextView"
style="@style/TextAppearance.Vector.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/qr_code_login_connecting_to_device" />
<include layout="@layout/item_loading" />
</LinearLayout>
<im.vector.app.features.login.qr.QrCodeLoginHeaderView
android:id="@+id/qrCodeLoginStatusHeaderView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:qrCodeLoginHeaderDescription="@string/qr_code_login_header_connected_description"
app:qrCodeLoginHeaderImageBackgroundTint="?colorPrimary"
app:qrCodeLoginHeaderImageResource="@drawable/ic_qr_code_login_connected"
app:qrCodeLoginHeaderTitle="@string/qr_code_login_header_connected_title" />
<TextView
android:id="@+id/qrCodeLoginStatusSecurityCode"
style="@style/TextAppearance.Vector.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginStatusHeaderView"
tools:text="28E-1B9-D0F-896"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/qrCodeLoginStatusNoMatchLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toTopOf="@id/qrCodeLoginStatusCancelButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="center_vertical"
android:background="@drawable/divider_horizontal" />
<TextView
android:id="@+id/qrCodeLoginStatusNoMatchTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?android:colorBackground"
android:paddingHorizontal="12dp"
android:text="@string/qr_code_login_status_no_match"
app:drawableLeftCompat="@drawable/divider_horizontal"
app:drawableTint="@color/alert_default_error_background" />
</FrameLayout>
<Button
android:id="@+id/qrCodeLoginStatusTryAgainButton"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="@string/qr_code_login_try_again"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/qrCodeLoginStatusNoMatchLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/qrCodeLoginConfirmSecurityCodeLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/qrCodeLoginStatusCancelButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible">
<ImageView
android:id="@+id/qrCodeLoginConfirmSecurityCodeImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:src="@drawable/ic_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/qrCodeLoginConfirmSecurityCodeTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:layout_marginStart="8dp"
android:text="@string/qr_code_login_confirm_security_code_description"
app:layout_constraintStart_toEndOf="@id/qrCodeLoginConfirmSecurityCodeImageView"
app:layout_constraintTop_toTopOf="@id/qrCodeLoginConfirmSecurityCodeImageView" />
<Button
android:id="@+id/qrCodeLoginConfirmSecurityCodeButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="16dp"
android:text="@string/qr_code_login_confirm_security_code"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginConfirmSecurityCodeTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/qrCodeLoginStatusCancelButton"
style="@style/Widget.Vector.Button.Outlined.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:text="@string/action_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -111,47 +110,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions" /> app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions" />
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
android:id="@+id/deviceListHeaderSignInWithQrCode"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListOtherSessions"
app:sessionsListHeaderDescription="@string/device_manager_sessions_sign_in_with_qr_code_description"
app:sessionsListHeaderHasLearnMoreLink="false"
app:sessionsListHeaderTitle="@string/device_manager_sessions_sign_in_with_qr_code_title"
tools:visibility="visible" />
<Button
android:id="@+id/deviceListHeaderScanQrCodeButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="12dp"
android:text="@string/qr_code_login_scan_qr_code_button"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderSignInWithQrCode"
tools:visibility="visible" />
<Button
android:id="@+id/deviceListHeaderShowQrCodeButton"
style="@style/Widget.Vector.Button.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"
android:text="@string/qr_code_login_show_qr_code_button"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderScanQrCodeButton"
tools:visibility="visible" />
<include <include
android:id="@+id/waiting_view" android:id="@+id/waiting_view"
layout="@layout/merge_overlay_waiting_view" layout="@layout/merge_overlay_waiting_view"

View file

@ -162,28 +162,6 @@ class OnboardingViewModelTest {
.finish() .finish()
} }
@Test
fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
val test = viewModel.test()
fakeVectorFeatures.givenCombinedLoginEnabled()
givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED)
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
test
.assertStatesChanges(
initialState,
{ copy(onboardingFlow = OnboardingFlow.SignIn) },
{ copy(isLoading = true) },
{ copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) },
{ copy(signMode = SignMode.SignIn) },
{ copy(canLoginWithQrCode = true) },
{ copy(isLoading = false) }
)
.assertEvents(OnboardingViewEvents.OpenCombinedLogin)
.finish()
}
@Test @Test
fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest { fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest {
val test = viewModel.test() val test = viewModel.test()