diff --git a/changelog.d/7338.wip b/changelog.d/7338.wip new file mode 100644 index 0000000000..fc47ecb2f9 --- /dev/null +++ b/changelog.d/7338.wip @@ -0,0 +1 @@ +Implement QR Code Login UI diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 549d176ad3..74ec175d17 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2183,6 +2183,7 @@ If you don’t know your password, go back to reset it. This is not a valid user identifier. Expected format: \'@user:homeserver.org\' Unable to find a valid homeserver. Please check your identifier + Scan QR code Seen by @@ -3330,6 +3331,9 @@ Session name Custom session names can help you recognize your devices more easily. Please be aware that session names are also visible to people you communicate with. + Sign in with QR Code + You can use this device to sign in a mobile or web device with a QR code. There are two ways to do this: + Inactive sessions 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. Unverified sessions @@ -3364,6 +3368,39 @@ Tap top right to see the option to feedback. Try it out + 1 + 2 + 3 + + + Scan QR code + Use the camera on this device to scan the QR code shown on your other device: + Sign in with QR code + Use your signed in device to scan the QR code below: + Scan the QR code below with your device that’s signed out. + Secure connection established + Check your signed in device, the code below should be displayed. Confirm that the code below matches with that device: + Unsuccessful connection + Linking with this device is not supported. + The linking wasn’t completed in the required time. + The request was denied on the other device. + Open ${app_name} on your other device + Go to Settings -> Security & Privacy -> Show All Sessions + Select \'Show QR code in this device\' + Start at the sign in screen + Select \'Sign in with QR code\' + Start at the sign in screen + Select \'Scan QR code\' + Show QR code in this device + Signing in a mobile device? + Scan QR code + Connecting to device + Signing you in + No match? + Try again + Confirm + Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account. + Apply bold format Apply italic format diff --git a/library/ui-styles/src/main/res/values/stylable_qr_code_instructions_view.xml b/library/ui-styles/src/main/res/values/stylable_qr_code_instructions_view.xml new file mode 100644 index 0000000000..c9a4bb9d05 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_qr_code_instructions_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/library/ui-styles/src/main/res/values/stylable_qr_code_login_header_view.xml b/library/ui-styles/src/main/res/values/stylable_qr_code_login_header_view.xml new file mode 100644 index 0000000000..99f56084d9 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_qr_code_login_header_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index e490311b91..252c33a8c4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index b5d6d891e4..8c14ca892a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 5b12e3bdc3..5449c0a735 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 915b25134b..5e133fab9c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -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. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index aef482ae2e..9a2c32f97c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -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() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 184a0108b9..63fa101c45 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -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, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo039.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo039.kt new file mode 100644 index 0000000000..190a71c9be --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo039.kt @@ -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() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 9d90973f8a..cfa02b2c74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index add69dd8c7..2c3cb440b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -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) { diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 5f34a349d6..16e26ff3b5 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -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, diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 6062a1f999..5c497c24ec 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -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") } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6b95b99467..b0cd202d12 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -323,6 +323,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index d2afdb65e8..97590028d8 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index f59f5afdea..255ac6d188 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt new file mode 100644 index 0000000000..8854d0720f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt new file mode 100644 index 0000000000..f5fd17c0c8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginActivity.kt @@ -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) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginArgs.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginArgs.kt new file mode 100644 index 0000000000..6c23d07c0f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginArgs.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt new file mode 100644 index 0000000000..330562b874 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginConnectionStatus.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginErrorType.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginErrorType.kt new file mode 100644 index 0000000000..9a6cc13de0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginErrorType.kt @@ -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, +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginHeaderView.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginHeaderView.kt new file mode 100644 index 0000000000..03478d2f50 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginHeaderView.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt new file mode 100644 index 0000000000..efd23f2530 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsFragment.kt @@ -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() { + + 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), + ) + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsView.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsView.kt new file mode 100644 index 0000000000..ed5c4de175 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginInstructionsView.kt @@ -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?) { + 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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginShowQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginShowQrCodeFragment.kt new file mode 100644 index 0000000000..d31f531a49 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginShowQrCodeFragment.kt @@ -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() { + + 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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt new file mode 100644 index 0000000000..a9c589e469 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginStatusFragment.kt @@ -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() { + + 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 */ } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginType.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginType.kt new file mode 100644 index 0000000000..b4bb5b667f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginType.kt @@ -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, +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt new file mode 100644 index 0000000000..dc258408e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewEvents.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt new file mode 100644 index 0000000000..e979ffa63c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewModel.kt @@ -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(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: QrCodeLoginViewState): QrCodeLoginViewModel + } + + companion object : MavericksViewModelFactory 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" + } +} diff --git a/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewState.kt b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewState.kt new file mode 100644 index 0000000000..0c4457c12f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/qr/QrCodeLoginViewState.kt @@ -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, + ) +} diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 53ed307da9..3970af385e 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 3521a02775..1d67f883a3 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -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 ) + + fun openLoginWithQrCode( + context: Context, + qrCodeLoginArgs: QrCodeLoginArgs, + ) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 9bb52fb1a5..46b14c6d5f 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -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()) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index 99678ea5c1..de10852238 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index 6877810f0a..aad54877c9 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -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() { diff --git a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt index e24c57c6de..3167eebc9f 100644 --- a/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/qrcode/QrCodeScannerViewModel.kt @@ -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(initialState) { @AssistedFactory diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index bd68cbc0ce..c507699e0b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -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() diff --git a/vector/src/main/res/drawable/circle_qr_code_login_instruction_with_border.xml b/vector/src/main/res/drawable/circle_qr_code_login_instruction_with_border.xml new file mode 100644 index 0000000000..cb99e4467c --- /dev/null +++ b/vector/src/main/res/drawable/circle_qr_code_login_instruction_with_border.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_qr_code.xml b/vector/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 0000000000..1ebdc169c9 --- /dev/null +++ b/vector/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_qr_code_login_connected.xml b/vector/src/main/res/drawable/ic_qr_code_login_connected.xml new file mode 100644 index 0000000000..48f5c6a383 --- /dev/null +++ b/vector/src/main/res/drawable/ic_qr_code_login_connected.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_qr_code_login_failed.xml b/vector/src/main/res/drawable/ic_qr_code_login_failed.xml new file mode 100644 index 0000000000..f49e07c066 --- /dev/null +++ b/vector/src/main/res/drawable/ic_qr_code_login_failed.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml index 12943b4dc0..7eff92f4f9 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml @@ -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" /> +