Merge pull request #7338 from vector-im/feature/ons/qr_code_login_ui

QR Code Login UI
This commit is contained in:
Onuray Sahin 2022-10-17 17:20:07 +03:00 committed by GitHub
commit 47c87141b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1693 additions and 12 deletions

1
changelog.d/7338.wip Normal file
View File

@ -0,0 +1 @@
Implement QR Code Login UI

View File

@ -2183,6 +2183,7 @@
<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>
<string name="seen_by">Seen by</string>
@ -3330,6 +3331,9 @@
<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>
<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>
@ -3364,6 +3368,39 @@
<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>
<!-- 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_new_device_instruction_1">Open ${app_name} on your other device</string>
<string name="qr_code_login_new_device_instruction_2">Go to Settings -> Security &amp; Privacy -> Show All Sessions</string>
<string name="qr_code_login_new_device_instruction_3">Select \'Show QR code in this device\'</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>
<!-- WYSIWYG Composer -->
<string name="rich_text_editor_format_bold">Apply bold format</string>
<string name="rich_text_editor_format_italic">Apply italic format</string>

View File

@ -0,0 +1,11 @@
<?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

@ -0,0 +1,11 @@
<?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

@ -125,6 +125,12 @@ interface AuthenticationService {
deviceId: String? = null
): Session
/**
* @param homeServerConnectionConfig the information about the homeserver and other configuration
* Return true if qr code login is supported by the server, false otherwise.
*/
suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean
/**
* Authenticate using m.login.token method during sign in with QR code.
* @param homeServerConnectionConfig the information about the homeserver and other configuration

View File

@ -59,7 +59,12 @@ data class HomeServerCapabilities(
/**
* True if the home server supports controlling the logout of all devices when changing password.
*/
val canControlLogoutDevices: Boolean = false
val canControlLogoutDevices: Boolean = false,
/**
* True if the home server supports login via qr code, false otherwise.
*/
val canLoginWithQrCode: Boolean = false,
) {
enum class RoomCapabilitySupport {

View File

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixIdFailure
import org.matrix.android.sdk.api.session.Session
@ -43,6 +44,7 @@ import org.matrix.android.sdk.internal.auth.login.QrLoginTokenTask
import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard
import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
import org.matrix.android.sdk.internal.di.Unauthenticated
@ -406,6 +408,20 @@ internal class DefaultAuthenticationService @Inject constructor(
)
}
override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val versions = runCatching {
executeRequest(null) {
authAPI.versions()
}
}
return if (versions.isSuccess) {
versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse()
} else {
false
}
}
override suspend fun loginUsingQrLoginToken(
homeServerConnectionConfig: HomeServerConnectionConfig,
loginToken: String,

View File

@ -53,6 +53,7 @@ private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882"
/**
* Return true if the SDK supports this homeserver version.
@ -78,6 +79,10 @@ internal fun Versions.doesServerSupportThreads(): Boolean {
return unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
}
internal fun Versions.doesServerSupportQrCodeLogin(): Boolean {
return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false
}
/**
* Return true if the server support the lazy loading of room members.
*

View File

@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
@ -63,7 +64,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 38L,
schemaVersion = 39L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
@ -111,5 +112,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 36) MigrateSessionTo036(realm).perform()
if (oldVersion < 37) MigrateSessionTo037(realm).perform()
if (oldVersion < 38) MigrateSessionTo038(realm).perform()
if (oldVersion < 39) MigrateSessionTo039(realm).perform()
}
}

View File

@ -43,7 +43,8 @@ internal object HomeServerCapabilitiesMapper {
defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
roomVersions = mapRoomVersion(entity.roomVersionsJson),
canUseThreading = entity.canUseThreading,
canControlLogoutDevices = entity.canControlLogoutDevices
canControlLogoutDevices = entity.canControlLogoutDevices,
canLoginWithQrCode = entity.canLoginWithQrCode,
)
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 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.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo039(realm: DynamicRealm) : RealmMigrator(realm, 39) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.CAN_LOGIN_WITH_QR_CODE, Boolean::class.java)
?.transform { obj ->
obj.set(HomeServerCapabilitiesEntityFields.CAN_LOGIN_WITH_QR_CODE, false)
}
?.forceRefreshOfHomeServerCapabilities()
}
}

View File

@ -30,7 +30,8 @@ internal open class HomeServerCapabilitiesEntity(
var defaultIdentityServerUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L,
var canUseThreading: Boolean = false,
var canControlLogoutDevices: Boolean = false
var canControlLogoutDevices: Boolean = false,
var canLoginWithQrCode: Boolean = false,
) : RealmObject() {
companion object

View File

@ -20,11 +20,11 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
@ -132,8 +132,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
}
homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
getVersionResult?.doesServerSupportThreads().orFalse()
}
if (getMediaConfigResult != null) {
@ -144,6 +142,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getVersionResult != null) {
homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
getVersionResult.doesServerSupportThreads()
homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin()
}
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {

View File

@ -85,6 +85,21 @@ 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,6 +76,15 @@ 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()
@ -138,5 +147,8 @@ 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")
}

View File

@ -323,6 +323,7 @@
<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" />
<!-- Services -->

View File

@ -60,6 +60,7 @@ 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
@ -662,6 +663,11 @@ 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,6 +40,9 @@ interface VectorFeatures {
* use [VectorPreferences.isNewAppLayoutEnabled] instead.
*/
fun isNewAppLayoutFeatureEnabled(): Boolean
fun isQrCodeLoginEnabled(): Boolean
fun isQrCodeLoginForAllServers(): Boolean
fun isReciprocateQrCodeLogin(): Boolean
fun isVoiceBroadcastEnabled(): Boolean
}
@ -56,5 +59,8 @@ 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 = false
}

View File

@ -0,0 +1,25 @@
/*
* 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()
}

View File

@ -0,0 +1,110 @@
/*
* 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.platform.SimpleFragmentActivity
import im.vector.lib.core.utils.compat.getParcelableCompat
import timber.log.Timber
@AndroidEntryPoint
class QrCodeLoginActivity : SimpleFragmentActivity() {
private val viewModel: QrCodeLoginViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
val qrCodeLoginArgs: QrCodeLoginArgs? = intent?.extras?.getParcelableCompat(Mavericks.KEY_ARG)
if (isFirstCreation()) {
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()
return
}
}
}
observeViewEvents()
}
private fun showInstructionsFragment(qrCodeLoginArgs: QrCodeLoginArgs) {
addFragment(
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()
}
}
}
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
)
}
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

@ -0,0 +1,26 @@
/*
* 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

@ -0,0 +1,24 @@
/*
* 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
sealed class QrCodeLoginConnectionStatus {
object ConnectingToDevice : QrCodeLoginConnectionStatus()
data class Connected(val securityCode: String, val canConfirmSecurityCode: Boolean) : QrCodeLoginConnectionStatus()
object SigningIn : QrCodeLoginConnectionStatus()
data class Failed(val errorType: QrCodeLoginErrorType, val canTryAgain: Boolean) : QrCodeLoginConnectionStatus()
}

View File

@ -0,0 +1,23 @@
/*
* 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 QrCodeLoginErrorType {
DEVICE_IS_NOT_SUPPORTED,
TIMEOUT,
REQUEST_WAS_DENIED,
}

View File

@ -0,0 +1,82 @@
/*
* 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.R
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,
R.styleable.QrCodeLoginHeaderView,
0,
0
).use {
setTitle(it)
setDescription(it)
setImage(it)
}
}
private fun setTitle(typedArray: TypedArray) {
val title = typedArray.getString(R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderTitle)
setTitle(title)
}
private fun setDescription(typedArray: TypedArray) {
val description = typedArray.getString(R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderDescription)
setDescription(description)
}
private fun setImage(typedArray: TypedArray) {
val imageResource = typedArray.getResourceId(R.styleable.QrCodeLoginHeaderView_qrCodeLoginHeaderImageResource, 0)
val backgroundTint = typedArray.getColor(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

@ -0,0 +1,100 @@
/*
* 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.R
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 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)
if (wasQrCode && !scannedQrCode.isNullOrBlank()) {
onQrCodeScanned(scannedQrCode)
} else {
onQrCodeScannerFailed()
}
}
}
private fun onQrCodeScanned(scannedQrCode: String) {
viewModel.handle(QrCodeLoginAction.OnQrCodeScanned(scannedQrCode))
}
private fun onQrCodeScannerFailed() {
Timber.d("QrCodeLoginInstructionsFragment.onQrCodeScannerFailed")
}
override fun invalidate() = withState(viewModel) { state ->
if (state.loginType == QrCodeLoginType.LOGIN) {
views.qrCodeLoginInstructionsView.setInstructions(
listOf(
getString(R.string.qr_code_login_new_device_instruction_1),
getString(R.string.qr_code_login_new_device_instruction_2),
getString(R.string.qr_code_login_new_device_instruction_3),
)
)
} else {
views.qrCodeLoginInstructionsView.setInstructions(
listOf(
getString(R.string.qr_code_login_link_a_device_scan_qr_code_instruction_1),
getString(R.string.qr_code_login_link_a_device_scan_qr_code_instruction_2),
)
)
}
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.R
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,
R.styleable.QrCodeLoginInstructionsView,
0,
0
).use {
setInstructions(it)
}
}
private fun setInstructions(typedArray: TypedArray) {
val instruction1 = typedArray.getString(R.styleable.QrCodeLoginInstructionsView_qrCodeLoginInstruction1)
val instruction2 = typedArray.getString(R.styleable.QrCodeLoginInstructionsView_qrCodeLoginInstruction2)
val instruction3 = typedArray.getString(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

@ -0,0 +1,82 @@
/*
* 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.R
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginShowQrCodeBinding
@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(R.string.qr_code_login_header_show_qr_code_new_device_description))
views.qrCodeLoginShowQrCodeInstructionsView.setInstructions(
listOf(
getString(R.string.qr_code_login_new_device_instruction_1),
getString(R.string.qr_code_login_new_device_instruction_2),
getString(R.string.qr_code_login_new_device_instruction_3),
)
)
} else {
views.qrCodeLoginShowQrCodeHeaderView.setDescription(getString(R.string.qr_code_login_header_show_qr_code_link_a_device_description))
views.qrCodeLoginShowQrCodeInstructionsView.setInstructions(
listOf(
getString(R.string.qr_code_login_link_a_device_show_qr_code_instruction_1),
getString(R.string.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

@ -0,0 +1,129 @@
/*
* 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
@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()
}
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(R.string.qr_code_login_header_failed_title))
views.qrCodeLoginStatusHeaderView.setDescription(getErrorCode(connectionStatus.errorType))
views.qrCodeLoginStatusHeaderView.setImage(
imageResource = R.drawable.ic_qr_code_login_failed,
backgroundTintColor = ThemeUtils.getColor(requireContext(), R.attr.colorError)
)
}
private fun getErrorCode(errorType: QrCodeLoginErrorType): String {
return when (errorType) {
QrCodeLoginErrorType.DEVICE_IS_NOT_SUPPORTED -> getString(R.string.qr_code_login_header_failed_device_is_not_supported_description)
QrCodeLoginErrorType.TIMEOUT -> getString(R.string.qr_code_login_header_failed_timeout_description)
QrCodeLoginErrorType.REQUEST_WAS_DENIED -> getString(R.string.qr_code_login_header_failed_denied_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(R.string.qr_code_login_connecting_to_device)
}
private fun handleSigningIn() {
views.qrCodeLoginConfirmSecurityCodeLayout.isVisible = false
views.qrCodeLoginStatusLoadingLayout.isVisible = true
views.qrCodeLoginStatusHeaderView.apply {
isVisible = true
setTitle(getString(R.string.dialog_title_success))
setDescription("")
setImage(R.drawable.ic_tick, ThemeUtils.getColor(requireContext(), R.attr.colorPrimary))
}
views.qrCodeLoginStatusSecurityCode.isVisible = false
views.qrCodeLoginStatusNoMatchLayout.isVisible = false
views.qrCodeLoginStatusCancelButton.isVisible = false
views.qrCodeLoginStatusTryAgainButton.isVisible = false
views.qrCodeLoginStatusLoadingTextView.setText(R.string.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(R.string.qr_code_login_header_connected_title))
views.qrCodeLoginStatusHeaderView.setDescription(getString(R.string.qr_code_login_header_connected_description))
views.qrCodeLoginStatusHeaderView.setImage(
imageResource = R.drawable.ic_qr_code_login_connected,
backgroundTintColor = ThemeUtils.getColor(requireContext(), 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

@ -0,0 +1,22 @@
/*
* 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

@ -0,0 +1,24 @@
/*
* 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()
}

View File

@ -0,0 +1,106 @@
/*
* 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.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import timber.log.Timber
class QrCodeLoginViewModel @AssistedInject constructor(
@Assisted private val initialState: QrCodeLoginViewState,
) : VectorViewModel<QrCodeLoginViewState, QrCodeLoginAction, QrCodeLoginViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<QrCodeLoginViewModel, QrCodeLoginViewState> {
override fun create(initialState: QrCodeLoginViewState): QrCodeLoginViewModel
}
companion object : MavericksViewModelFactory<QrCodeLoginViewModel, QrCodeLoginViewState> by hiltMavericksViewModelFactory()
override fun handle(action: QrCodeLoginAction) {
when (action) {
is QrCodeLoginAction.OnQrCodeScanned -> handleOnQrCodeScanned(action)
QrCodeLoginAction.GenerateQrCode -> handleQrCodeViewStarted()
QrCodeLoginAction.ShowQrCode -> handleShowQrCode()
}
}
private fun handleShowQrCode() {
_viewEvents.post(QrCodeLoginViewEvents.NavigateToShowQrCodeScreen)
}
private fun handleQrCodeViewStarted() {
val qrCodeData = generateQrCodeData()
setState {
copy(
generatedQrCodeData = qrCodeData
)
}
}
private fun handleOnQrCodeScanned(action: QrCodeLoginAction.OnQrCodeScanned) {
if (isValidQrCode(action.qrCode)) {
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.ConnectingToDevice
)
}
_viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
}
}
private fun onFailed(errorType: QrCodeLoginErrorType, canTryAgain: Boolean) {
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.Failed(errorType, canTryAgain)
)
}
}
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
)
}
}
// TODO. Implement in the logic related PR.
private fun isValidQrCode(qrCode: String): Boolean {
Timber.d("isValidQrCode: $qrCode")
return false
}
// TODO. Implement in the logic related PR.
private fun generateQrCodeData(): String {
return "TODO"
}
}

View File

@ -0,0 +1,30 @@
/*
* 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

@ -70,6 +70,8 @@ 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
@ -604,6 +606,14 @@ 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,6 +31,7 @@ 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
@ -201,4 +202,9 @@ interface Navigator {
screenCaptureIntent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>
)
fun openLoginWithQrCode(
context: Context,
qrCodeLoginArgs: QrCodeLoginArgs,
)
}

View File

@ -118,6 +118,35 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun checkQrCodeLoginCapability(homeServerUrl: String) {
if (!vectorFeatures.isQrCodeLoginEnabled()) {
setState {
copy(
canLoginWithQrCode = false
)
}
} else if (vectorFeatures.isQrCodeLoginForAllServers()) {
// allow for all servers
setState {
copy(
canLoginWithQrCode = true
)
}
} else {
viewModelScope.launch {
// check if selected server supports MSC3882 first
homeServerConnectionConfigFactory.create(homeServerUrl)?.let {
val canLoginWithQrCode = authenticationService.isQrLoginSupported(it)
setState {
copy(
canLoginWithQrCode = canLoginWithQrCode
)
}
}
}
}
}
private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
private val defaultHomeserverUrl = matrixOrgUrl
@ -680,6 +709,7 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString())
}
}

View File

@ -58,7 +58,9 @@ data class OnboardingViewState(
val selectedAuthenticationState: SelectedAuthenticationState = SelectedAuthenticationState(),
@PersistState
val personalizationState: PersonalizationState = PersonalizationState()
val personalizationState: PersonalizationState = PersonalizationState(),
val canLoginWithQrCode: Boolean = false,
) : MavericksState
enum class OnboardingFlow {

View File

@ -36,10 +36,13 @@ import im.vector.app.core.extensions.setOnFocusLostListener
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.databinding.FragmentFtueCombinedLoginBinding
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.SsoState
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
@ -55,6 +58,7 @@ class FtueAuthCombinedLoginFragment :
@Inject lateinit var loginFieldsValidation: LoginFieldsValidation
@Inject lateinit var loginErrorParser: LoginErrorParser
@Inject lateinit var vectorFeatures: VectorFeatures
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedLoginBinding {
return FragmentFtueCombinedLoginBinding.inflate(inflater, container, false)
@ -70,6 +74,26 @@ 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

@ -24,11 +24,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorDummyViewState
import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.session.Session
class QrCodeScannerViewModel @AssistedInject constructor(
@Assisted initialState: VectorDummyViewState,
val session: Session
) : VectorViewModel<VectorDummyViewState, QrCodeScannerAction, QrCodeScannerEvents>(initialState) {
@AssistedFactory

View File

@ -35,8 +35,11 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.VectorFeatures
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet
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
@ -63,6 +66,8 @@ class VectorSettingsDevicesFragment :
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var vectorFeatures: VectorFeatures
@Inject lateinit var stringProvider: StringProvider
private val viewModel: DevicesViewModel by fragmentViewModel()
@ -88,6 +93,7 @@ class VectorSettingsDevicesFragment :
initWaitingView()
initOtherSessionsView()
initSecurityRecommendationsView()
initQrLoginView()
observeViewEvents()
}
@ -152,6 +158,38 @@ 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

@ -0,0 +1,14 @@
<?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

@ -0,0 +1,9 @@
<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

@ -0,0 +1,9 @@
<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

@ -0,0 +1,10 @@
<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

@ -194,9 +194,9 @@
android:text="@string/ftue_auth_forgot_password"
android:textAllCaps="true"
android:textColor="?colorSecondary"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/loginPasswordInput" />
@ -244,6 +244,20 @@
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"
@ -252,7 +266,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
app:layout_constraintTop_toBottomOf="@id/ssoButtonsHeader"
app:layout_constraintTop_toBottomOf="@id/loginWithQrCode"
tools:signMode="signup" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,84 @@
<?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

@ -0,0 +1,53 @@
<?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

@ -0,0 +1,155 @@
<?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,6 +1,7 @@
<?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">
@ -108,6 +109,47 @@
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:sessionsListHeaderHasLearnMoreLink="false"
app:sessionsListHeaderDescription="@string/device_manager_sessions_sign_in_with_qr_code_description"
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

@ -0,0 +1,48 @@
<?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

@ -0,0 +1,101 @@
<?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>