Merge pull request #8890 from element-hq/feature/bma/removeLegacyQrCodeLogin

Remove legacy qr code login
This commit is contained in:
Benoit Marty 2024-08-27 09:36:14 +02:00 committed by GitHub
commit 76616b1a28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 82 additions and 2948 deletions

1
changelog.d/8889.misc Normal file
View file

@ -0,0 +1 @@
Remove legacy QR code login.

View file

@ -2228,7 +2228,8 @@
<string name="login_signin_matrix_id_password_notice">If you dont know your password, go back to reset it.</string>
<string name="login_signin_matrix_id_error_invalid_matrix_id">This is not a valid user identifier. Expected format: \'@user:homeserver.org\'</string>
<string name="autodiscover_well_known_error">Unable to find a valid homeserver. Please check your identifier</string>
<string name="login_scan_qr_code">Scan QR code</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="login_scan_qr_code">Scan QR code</string>
<string name="seen_by">Seen by</string>
@ -3475,9 +3476,10 @@
<string name="device_manager_session_rename_edit_hint">Session name</string>
<string name="device_manager_session_rename_description">Custom session names can help you recognize your devices more easily.</string>
<string name="device_manager_session_rename_warning">Please be aware that session names are also visible to people you communicate with.</string>
<string name="device_manager_sessions_sign_in_with_qr_code_title">Sign in with QR Code</string>
<string name="device_manager_sessions_sign_in_with_qr_code_description">You can use this device to sign in a mobile or web device with a QR code. There are two ways to do this:</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="device_manager_sessions_sign_in_with_qr_code_title">Sign in with QR Code</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="device_manager_sessions_sign_in_with_qr_code_description">You can use this device to sign in a mobile or web device with a QR code. There are two ways to do this:</string>
<string name="device_manager_learn_more_sessions_inactive_title">Inactive sessions</string>
<string name="device_manager_learn_more_sessions_inactive">Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.</string>
<string name="device_manager_learn_more_sessions_unverified_title">Unverified sessions</string>
@ -3515,45 +3517,82 @@
<string name="onboarding_new_app_layout_feedback_message">Tap top right to see the option to feedback.</string>
<string name="onboarding_new_app_layout_button_try">Try it out</string>
<string name="one">1</string>
<string name="two">2</string>
<string name="three">3</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="one">1</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="two">2</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="three">3</string>
<!-- QR Code Login -->
<string name="qr_code_login_header_scan_qr_code_title">Scan QR code</string>
<string name="qr_code_login_header_scan_qr_code_description">Use the camera on this device to scan the QR code shown on your other device:</string>
<string name="qr_code_login_header_show_qr_code_title">Sign in with QR code</string>
<string name="qr_code_login_header_show_qr_code_new_device_description">Use your signed in device to scan the QR code below:</string>
<string name="qr_code_login_header_show_qr_code_link_a_device_description">Scan the QR code below with your device thats signed out.</string>
<string name="qr_code_login_header_connected_title">Secure connection established</string>
<string name="qr_code_login_header_connected_description">Check your signed in device, the code below should be displayed. Confirm that the code below matches with that device:</string>
<string name="qr_code_login_header_failed_title">Unsuccessful connection</string>
<string name="qr_code_login_header_failed_device_is_not_supported_description">Linking with this device is not supported.</string>
<string name="qr_code_login_header_failed_timeout_description">The linking wasnt completed in the required time.</string>
<string name="qr_code_login_header_failed_denied_description">The request was denied on the other device.</string>
<string name="qr_code_login_header_failed_other_description">The request failed.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your internet connection(s); Your device(s);</string>
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">The other device is already signed in.</string>
<string name="qr_code_login_header_failed_other_device_not_signed_in_description">The other device must be signed in.</string>
<string name="qr_code_login_header_failed_invalid_qr_code_description">That QR code is invalid.</string>
<string name="qr_code_login_header_failed_user_cancelled_description">The sign in was cancelled on the other device.</string>
<string name="qr_code_login_header_failed_homeserver_is_not_supported_description">The homeserver doesn\'t support sign in with QR code.</string>
<string name="qr_code_login_new_device_instruction_1">Open the app on your other device</string>
<string name="qr_code_login_new_device_instruction_2">Go to Settings -> Security &amp; Privacy</string>
<string name="qr_code_login_new_device_instruction_3">Select \'Show QR code\'</string>
<string name="qr_code_login_link_a_device_scan_qr_code_instruction_1">Start at the sign in screen</string>
<string name="qr_code_login_link_a_device_scan_qr_code_instruction_2">Select \'Sign in with QR code\'</string>
<string name="qr_code_login_link_a_device_show_qr_code_instruction_1">Start at the sign in screen</string>
<string name="qr_code_login_link_a_device_show_qr_code_instruction_2">Select \'Scan QR code\'</string>
<string name="qr_code_login_show_qr_code_button">Show QR code in this device</string>
<string name="qr_code_login_signing_in_a_mobile_device">Signing in a mobile device?</string>
<string name="qr_code_login_scan_qr_code_button">Scan QR code</string>
<string name="qr_code_login_connecting_to_device">Connecting to device</string>
<string name="qr_code_login_signing_in">Signing you in</string>
<string name="qr_code_login_status_no_match">No match?</string>
<string name="qr_code_login_try_again">Try again</string>
<string name="qr_code_login_confirm_security_code">Confirm</string>
<string name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_scan_qr_code_title">Scan QR code</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_scan_qr_code_description">Use the camera on this device to scan the QR code shown on your other device:</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_show_qr_code_title">Sign in with QR code</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_show_qr_code_new_device_description">Use your signed in device to scan the QR code below:</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_show_qr_code_link_a_device_description">Scan the QR code below with your device thats signed out.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_connected_title">Secure connection established</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_connected_description">Check your signed in device, the code below should be displayed. Confirm that the code below matches with that device:</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_title">Unsuccessful connection</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_device_is_not_supported_description">Linking with this device is not supported.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_timeout_description">The linking wasnt completed in the required time.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_denied_description">The request was denied on the other device.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_other_description">The request failed.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_e2ee_security_issue_description">A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your internet connection(s); Your device(s);</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_other_device_already_signed_in_description">The other device is already signed in.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_other_device_not_signed_in_description">The other device must be signed in.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_invalid_qr_code_description">That QR code is invalid.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_user_cancelled_description">The sign in was cancelled on the other device.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_header_failed_homeserver_is_not_supported_description">The homeserver doesn\'t support sign in with QR code.</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_new_device_instruction_1">Open the app on your other device</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_new_device_instruction_2">Go to Settings -> Security &amp; Privacy</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_new_device_instruction_3">Select \'Show QR code\'</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_link_a_device_scan_qr_code_instruction_1">Start at the sign in screen</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_link_a_device_scan_qr_code_instruction_2">Select \'Sign in with QR code\'</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_link_a_device_show_qr_code_instruction_1">Start at the sign in screen</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_link_a_device_show_qr_code_instruction_2">Select \'Scan QR code\'</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_show_qr_code_button">Show QR code in this device</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_signing_in_a_mobile_device">Signing in a mobile device?</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_scan_qr_code_button">Scan QR code</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_connecting_to_device">Connecting to device</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_signing_in">Signing you in</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_status_no_match">No match?</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_try_again">Try again</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_confirm_security_code">Confirm</string>
<!-- TODO TO BE REMOVED -->
<string tools:ignore="UnusedResources" name="qr_code_login_confirm_security_code_description">Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account.</string>
<!-- Rich text editor -->
<string name="rich_text_editor_format_bold">Apply bold format</string>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="QrCodeLoginInstructionsView">
<attr name="qrCodeLoginInstruction1" format="string" />
<attr name="qrCodeLoginInstruction2" format="string" />
<attr name="qrCodeLoginInstruction3" format="string" />
<attr name="qrCodeLoginInstruction4" format="string" />
</declare-styleable>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="QrCodeLoginHeaderView">
<attr name="qrCodeLoginHeaderTitle" format="string" />
<attr name="qrCodeLoginHeaderDescription" format="string" />
<attr name="qrCodeLoginHeaderImageResource" format="reference" />
<attr name="qrCodeLoginHeaderImageBackgroundTint" format="color" />
</declare-styleable>
</resources>

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,
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(
label = "Enable Voice Broadcast",
key = DebugFeatureKeys.voiceBroadcastEnabled,

View file

@ -76,15 +76,6 @@ class DebugVectorFeatures(
override fun isNewAppLayoutFeatureEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled)
?: 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)
?: vectorFeatures.isVoiceBroadcastEnabled()
@ -150,9 +141,6 @@ object DebugFeatureKeys {
val screenSharing = booleanPreferencesKey("screen-sharing")
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
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 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.details.SessionDetailsActivity" />
<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" />
<!-- 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.preview.LocationPreviewViewModel
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.media.VectorAttachmentViewerViewModel
import im.vector.app.features.onboarding.OnboardingViewModel
@ -668,11 +667,6 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(RenameSessionViewModel::class)
fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(QrCodeLoginViewModel::class)
fun qrCodeLoginViewModelFactory(factory: QrCodeLoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(SessionLearnMoreViewModel::class)

View file

@ -40,9 +40,6 @@ interface VectorFeatures {
* use [VectorPreferences.isNewAppLayoutEnabled] instead.
*/
fun isNewAppLayoutFeatureEnabled(): Boolean
fun isQrCodeLoginEnabled(): Boolean
fun isQrCodeLoginForAllServers(): Boolean
fun isReciprocateQrCodeLogin(): Boolean
fun isVoiceBroadcastEnabled(): Boolean
fun isUnverifiedSessionsAlertEnabled(): Boolean
}
@ -60,9 +57,6 @@ class DefaultVectorFeatures : VectorFeatures {
override fun isLocationSharingEnabled() = Config.ENABLE_LOCATION_SHARING
override fun forceUsageOfOpusEncoder(): Boolean = false
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 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.login.LoginActivity
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.OriginOfMatrixTo
import im.vector.app.features.media.AttachmentData
@ -614,14 +612,6 @@ class DefaultNavigator @Inject constructor(
activityResultLauncher.launch(screenCaptureIntent)
}
override fun openLoginWithQrCode(context: Context, qrCodeLoginArgs: QrCodeLoginArgs) {
QrCodeLoginActivity
.getIntent(context, qrCodeLoginArgs)
.also {
context.startActivity(it)
}
}
private fun Intent.start(context: Context) {
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.LocationSharingMode
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.media.AttachmentData
import im.vector.app.features.pin.PinMode
@ -202,9 +201,4 @@ interface Navigator {
screenCaptureIntent: 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 defaultHomeserverUrl = mdmService.getData(MdmData.DefaultHomeserverUrl, matrixOrgUrl)
@ -710,7 +687,6 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend {
checkQrCodeLoginCapability()
postAction()
})
}

View file

@ -59,8 +59,6 @@ data class OnboardingViewState(
@PersistState
val personalizationState: PersonalizationState = PersonalizationState(),
val canLoginWithQrCode: Boolean = false,
) : MavericksState
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.SSORedirectRouterActivity
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.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
@ -75,26 +73,6 @@ class FtueAuthCombinedLoginFragment :
viewModel.handle(OnboardingAction.UserNameEnteredAction.Login(views.loginInput.content()))
}
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() {

View file

@ -41,8 +41,6 @@ import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.auth.ReAuthActivity
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.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
@ -106,7 +104,6 @@ class VectorSettingsDevicesFragment :
initOtherSessionsHeaderView()
initOtherSessionsView()
initSecurityRecommendationsView()
initQrLoginView()
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() {
cleanUpLearnMoreButtonsListeners()
super.onDestroyView()

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadius="0dp"
android:thicknessRatio="2"
android:useLevel="false">
<solid android:color="?android:colorBackground" />
<stroke
android:width="1dp"
android:color="?colorPrimary" />
</shape>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="12dp"
android:viewportWidth="13"
android:viewportHeight="12">
<path
android:pathData="M7.167,12V10.667H8.5V12H7.167ZM5.833,10.667V7.333H7.167V10.667H5.833ZM11.167,8.667V6H12.5V8.667H11.167ZM9.833,6V4.667H11.167V6H9.833ZM1.833,7.333V6H3.167V7.333H1.833ZM0.5,6V4.667H1.833V6H0.5ZM6.5,1.333V0H7.833V1.333H6.5ZM1.333,3.167H3.667V0.833H1.333V3.167ZM1,4C0.856,4 0.736,3.953 0.642,3.858C0.547,3.764 0.5,3.644 0.5,3.5V0.5C0.5,0.356 0.547,0.236 0.642,0.142C0.736,0.047 0.856,0 1,0H4C4.144,0 4.264,0.047 4.358,0.142C4.453,0.236 4.5,0.356 4.5,0.5V3.5C4.5,3.644 4.453,3.764 4.358,3.858C4.264,3.953 4.144,4 4,4H1ZM1.333,11.167H3.667V8.833H1.333V11.167ZM1,12C0.856,12 0.736,11.953 0.642,11.858C0.547,11.764 0.5,11.644 0.5,11.5V8.5C0.5,8.356 0.547,8.236 0.642,8.142C0.736,8.047 0.856,8 1,8H4C4.144,8 4.264,8.047 4.358,8.142C4.453,8.236 4.5,8.356 4.5,8.5V11.5C4.5,11.644 4.453,11.764 4.358,11.858C4.264,11.953 4.144,12 4,12H1ZM9.333,3.167H11.667V0.833H9.333V3.167ZM9,4C8.856,4 8.736,3.953 8.642,3.858C8.547,3.764 8.5,3.644 8.5,3.5V0.5C8.5,0.356 8.547,0.236 8.642,0.142C8.736,0.047 8.856,0 9,0H12C12.144,0 12.264,0.047 12.358,0.142C12.453,0.236 12.5,0.356 12.5,0.5V3.5C12.5,3.644 12.453,3.764 12.358,3.858C12.264,3.953 12.144,4 12,4H9ZM9.833,12V10H8.5V8.667H11.167V10.667H12.5V12H9.833ZM7.167,7.333V6H9.833V7.333H7.167ZM4.5,7.333V6H3.167V4.667H7.167V6H5.833V7.333H4.5ZM5.167,4V1.333H6.5V2.667H7.833V4H5.167ZM2,2.5V1.5H3V2.5H2ZM2,10.5V9.5H3V10.5H2ZM10,2.5V1.5H11V2.5H10Z"
android:fillColor="#0DBD8B"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="54dp"
android:height="38dp"
android:viewportWidth="54"
android:viewportHeight="38">
<path
android:pathData="M53.667,19C53.667,17.533 52.467,16.333 51,16.333H45.48C44.173,7.293 36.413,0.333 27,0.333C17.587,0.333 9.827,7.293 8.52,16.333H3C1.533,16.333 0.333,17.533 0.333,19C0.333,20.467 1.533,21.667 3,21.667H8.52C9.827,30.707 17.587,37.667 27,37.667C36.413,37.667 44.173,30.707 45.48,21.667H51C52.467,21.667 53.667,20.467 53.667,19ZM35,25.667C35,27.133 33.8,28.333 32.333,28.333H21.667C20.2,28.333 19,27.133 19,25.667V17.667C19,16.2 20.2,15 21.667,15V12.333C21.667,9.107 24.547,6.52 27.907,7.08C30.52,7.507 32.333,9.96 32.333,12.627V15C33.8,15 35,16.2 35,17.667V25.667ZM29,21.667C29,22.76 28.093,23.667 27,23.667C25.907,23.667 25,22.76 25,21.667C25,20.573 25.907,19.667 27,19.667C28.093,19.667 29,20.573 29,21.667ZM29.667,12.333V15H24.333V12.333C24.333,10.867 25.533,9.667 27,9.667C28.467,9.667 29.667,10.867 29.667,12.333Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M20,40C31.046,40 40,31.046 40,20C40,8.954 31.046,0 20,0C8.954,0 0,8.954 0,20C0,31.046 8.954,40 20,40ZM18.084,14.688C18.007,13.809 18.655,13.037 19.535,12.976C20.399,12.914 21.17,13.562 21.263,14.441V14.688L20.769,20.86C20.723,21.431 20.244,21.863 19.674,21.863H19.581C19.041,21.816 18.624,21.4 18.578,20.86L18.084,14.688ZM21.015,24.887C21.015,25.637 20.407,26.244 19.657,26.244C18.907,26.244 18.3,25.637 18.3,24.887C18.3,24.137 18.907,23.529 19.657,23.529C20.407,23.529 21.015,24.137 21.015,24.887Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -244,20 +244,6 @@
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
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
android:id="@+id/ssoButtons"
android:layout_width="0dp"
@ -266,7 +252,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginWithQrCode"
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader"
tools:signMode="signup" />
</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"?>
<ScrollView 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">
@ -111,47 +110,6 @@
app:layout_constraintStart_toStartOf="parent"
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
android:id="@+id/waiting_view"
layout="@layout/merge_overlay_waiting_view"

View file

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/qrCodeLoginHeaderImageView"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="40dp"
android:background="@drawable/circle"
android:contentDescription="@string/qr_code_login_header_scan_qr_code_title"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?vctr_system"
tools:backgroundTint="?colorPrimary"
tools:src="@drawable/ic_camera" />
<TextView
android:id="@+id/qrCodeLoginHeaderTitleTextView"
style="@style/TextAppearance.Vector.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginHeaderImageView"
tools:text="@string/qr_code_login_header_scan_qr_code_title" />
<TextView
android:id="@+id/qrCodeLoginHeaderDescriptionTextView"
style="@style/TextAppearance.Vector.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/qrCodeLoginHeaderTitleTextView"
tools:text="@string/qr_code_login_header_scan_qr_code_description" />
</merge>

View file

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<LinearLayout
android:id="@+id/instructions1Layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView
style="@style/TextAppearance.Vector.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_qr_code_login_instruction_with_border"
android:padding="6dp"
android:text="@string/one"
android:textColor="?colorPrimary" />
<TextView
android:id="@+id/instruction1TextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
tools:text="@string/qr_code_login_new_device_instruction_1" />
</LinearLayout>
<LinearLayout
android:id="@+id/instructions2Layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/instructions1Layout"
tools:visibility="visible">
<TextView
style="@style/TextAppearance.Vector.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_qr_code_login_instruction_with_border"
android:padding="6dp"
android:text="@string/two"
android:textColor="?colorPrimary" />
<TextView
android:id="@+id/instruction2TextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
tools:text="@string/qr_code_login_new_device_instruction_2" />
</LinearLayout>
<LinearLayout
android:id="@+id/instructions3Layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/instructions2Layout"
tools:visibility="visible">
<TextView
style="@style/TextAppearance.Vector.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_qr_code_login_instruction_with_border"
android:padding="6dp"
android:text="@string/three"
android:textColor="?colorPrimary" />
<TextView
android:id="@+id/instruction3TextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
tools:text="@string/qr_code_login_new_device_instruction_3" />
</LinearLayout>
</merge>

View file

@ -162,28 +162,6 @@ class OnboardingViewModelTest {
.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
fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest {
val test = viewModel.test()