From 03428ea9f54f4a684d5251b228c6c2438794c4a5 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 3 Dec 2020 10:59:08 +0100 Subject: [PATCH 01/20] Social Login And new custom homeserver completion (and remember history) --- .../java/org/matrix/android/sdk/api/Matrix.kt | 4 + .../java/org/matrix/android/sdk/api/Matrix.kt | 4 + .../sdk/api/auth/HomeServerHistoryService.kt | 29 ++++ .../sdk/api/auth/data/IdentityProvider.kt | 42 +++++ .../sdk/api/auth/data/LoginFlowResult.kt | 1 + .../auth/DefaultAuthenticationService.kt | 2 + .../auth/DefaultHomeServerHistoryService.kt | 50 ++++++ .../internal/auth/data/LoginFlowResponse.kt | 11 +- .../registration/RegistrationFlowResponse.kt | 12 ++ .../database/model/KnownServerUrlEntity.kt | 27 ++++ .../sdk/internal/di/MatrixComponent.kt | 3 + .../sdk/internal/raw/GlobalRealmMigration.kt | 39 +++++ .../sdk/internal/raw/GlobalRealmModule.kt | 4 +- .../android/sdk/internal/raw/RawModule.kt | 7 + .../im/vector/app/core/di/FragmentModule.kt | 11 +- .../im/vector/app/core/di/VectorComponent.kt | 3 + .../im/vector/app/core/di/VectorModule.kt | 7 + .../vector/app/features/login/LoginAction.kt | 3 +- .../app/features/login/LoginActivity.kt | 55 +++---- .../app/features/login/LoginFragment.kt | 29 ++-- .../im/vector/app/features/login/LoginMode.kt | 32 +++- .../login/LoginServerSelectionFragment.kt | 2 +- .../login/LoginServerUrlFormFragment.kt | 14 +- .../LoginSignUpSignInSelectionFragment.kt | 125 ++++++++++++++- .../login/LoginSignUpSignInSsoFragment.kt | 99 ------------ .../app/features/login/LoginViewModel.kt | 105 +++++++----- .../app/features/login/LoginViewState.kt | 8 +- .../features/login/SocialLoginButtonsView.kt | 149 ++++++++++++++++++ .../signout/soft/SoftLogoutController.kt | 10 +- .../signout/soft/SoftLogoutFragment.kt | 21 ++- .../signout/soft/SoftLogoutViewModel.kt | 8 +- ...social_google_background_selector_dark.xml | 7 + ...ocial_google_background_selector_light.xml | 6 + .../src/main/res/drawable/ic_social_apple.xml | 12 ++ .../main/res/drawable/ic_social_facebook.xml | 12 ++ .../main/res/drawable/ic_social_github.xml | 12 ++ .../main/res/drawable/ic_social_google.xml | 36 +++++ .../main/res/drawable/ic_social_twitter.xml | 12 ++ vector/src/main/res/layout/fragment_login.xml | 37 ++++- .../layout/fragment_login_server_url_form.xml | 4 +- ...fragment_login_signup_signin_selection.xml | 35 +++- vector/src/main/res/values/attrs.xml | 14 ++ vector/src/main/res/values/colors_riotx.xml | 1 + vector/src/main/res/values/strings.xml | 6 + vector/src/main/res/values/styles_riot.xml | 98 ++++++++++++ vector/src/main/res/values/theme_dark.xml | 6 + vector/src/main/res/values/theme_light.xml | 7 + 47 files changed, 1001 insertions(+), 220 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/IdentityProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt delete mode 100644 vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt create mode 100644 vector/src/main/res/color/button_social_google_background_selector_dark.xml create mode 100644 vector/src/main/res/color/button_social_google_background_selector_light.xml create mode 100644 vector/src/main/res/drawable/ic_social_apple.xml create mode 100644 vector/src/main/res/drawable/ic_social_facebook.xml create mode 100644 vector/src/main/res/drawable/ic_social_github.xml create mode 100644 vector/src/main/res/drawable/ic_social_google.xml create mode 100644 vector/src/main/res/drawable/ic_social_twitter.xml diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index 0d71af864b..03943cea14 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -25,6 +25,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.common.DaggerTestMatrixComponent @@ -49,6 +50,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager + @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService private val uiHandler = Handler(Looper.getMainLooper()) @@ -71,6 +73,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun rawService() = rawService + fun homeServerHistoryService() = homeServerHistoryService + fun legacySessionImporter(): LegacySessionImporter { return legacySessionImporter } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index cf6f37cec8..a5d457222f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -23,6 +23,7 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.SessionManager @@ -47,6 +48,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver @Inject internal lateinit var olmManager: OlmManager @Inject internal lateinit var sessionManager: SessionManager + @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService init { Monarchy.init(context) @@ -65,6 +67,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun rawService() = rawService + fun homeServerHistoryService() = homeServerHistoryService + fun legacySessionImporter(): LegacySessionImporter { return legacySessionImporter } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt new file mode 100644 index 0000000000..a52d13d95c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.api.auth + +/** + * A simple service to remember homeservers you already connected to. + */ +interface HomeServerHistoryService { + + fun getKnownServersUrls(): List + + fun addHomeServerToHistory(url: String) + + fun clearHistory() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/IdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/IdentityProvider.kt new file mode 100644 index 0000000000..66c002e3c6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/IdentityProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.api.auth.data + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.android.parcel.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class IdentityProvider( + /** + * The id field would be opaque with the accepted characters matching unreserved URI characters as defined in RFC3986 + * - this was chosen to avoid having to encode special characters in the URL. Max length 128. + */ + @Json(name = "id") val id: String, + /** + * The name field should be the human readable string intended for printing by the client. + * */ + @Json(name = "name") val name: String?, + /** + * The icon field is the only optional field and should point to an icon representing the IdP. + * If present then it must be an HTTPS URL to an image resource. + * This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily. + */ + @Json(name = "icon") val icon: String? +) : Parcelable diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index 64d3ddcca5..53c18385f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data sealed class LoginFlowResult { data class Success( val supportedLoginTypes: List, + val ssoIdentityProviders: List?, val isLoginAndRegistrationSupported: Boolean, val homeServerUrl: String, val isOutdatedHomeserver: Boolean 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 3d5a0efcd4..ba3bd29b9f 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 @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult +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 @@ -278,6 +279,7 @@ internal class DefaultAuthenticationService @Inject constructor( } return LoginFlowResult.Success( loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, + loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.identityProvider, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl, !versions.isSupportedBySdk() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt new file mode 100644 index 0000000000..2f1da60768 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.internal.auth + +import com.zhuinden.monarchy.Monarchy +import io.realm.kotlin.where +import org.matrix.android.sdk.api.auth.HomeServerHistoryService +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity +import org.matrix.android.sdk.internal.di.GlobalDatabase +import javax.inject.Inject + +class DefaultHomeServerHistoryService @Inject constructor( + @GlobalDatabase private val monarchy: Monarchy +) : HomeServerHistoryService { + + override fun getKnownServersUrls(): List { + return monarchy.fetchAllMappedSync( + { realm -> + realm.where() + }, + { it.url } + ) + } + + override fun addHomeServerToHistory(url: String) { + monarchy.writeAsync { realm -> + KnownServerUrlEntity(url).let { + realm.insertOrUpdate(it) + } + } + } + + override fun clearHistory() { + monarchy.runTransactionSync { it.where().findAll().deleteAllFromRealm() } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 8acdee3608..54d38cd336 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.data import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.auth.data.IdentityProvider @JsonClass(generateAdapter = true) internal data class LoginFlowResponse( @@ -34,5 +35,13 @@ internal data class LoginFlow( * The login type. This is supplied as the type when logging in. */ @Json(name = "type") - val type: String? + val type: String?, + + /** + * Augments m.login.sso flow discovery definition to include metadata on the supported IDPs + * the client can show a button for each of the supported providers + * See MSC #2858 + */ + @Json(name = "identity_providers") + val identityProvider: List? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt index 5b105c4d40..3461a4d738 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt @@ -51,6 +51,18 @@ data class RegistrationFlowResponse( * The information that the client will need to know in order to use a given type of authentication. * For each login stage type presented, that type may be present as a key in this dictionary. * For example, the public key of reCAPTCHA stage could be given here. + * other example + * "params": { + * "m.login.sso": { + * "identity_providers": [ + * { + * "id": "google", + * "name": "Google", + * "icon": "https://..." + * } + * ] + * } + * } */ @Json(name = "params") val params: JsonDict? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt new file mode 100644 index 0000000000..1ebdc22ab4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/KnownServerUrlEntity.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2020 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.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class KnownServerUrlEntity( + @PrimaryKey + var url: String = "" +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt index f959104e11..9d6fa29bb2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt @@ -25,6 +25,7 @@ import okhttp3.OkHttpClient import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.AuthModule @@ -62,6 +63,8 @@ internal interface MatrixComponent { fun rawService(): RawService + fun homeServerHistoryService(): HomeServerHistoryService + fun context(): Context fun matrixConfiguration(): MatrixConfiguration diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt new file mode 100644 index 0000000000..aded751dba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 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 org.matrix.android.sdk.internal.raw + +import io.realm.DynamicRealm +import io.realm.RealmMigration +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields +import timber.log.Timber + +internal object GlobalRealmMigration : RealmMigration { + + // Current schema version + const val SCHEMA_VERSION = 1L + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") + + if (oldVersion <= 0) migrateTo1(realm) + } + + private fun migrateTo1(realm: DynamicRealm) { + realm.schema.create("KnownServerUrlEntity") + .addField(KnownServerUrlEntityFields.URL, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt index e4e4160193..770a49c904 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmModule.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.raw import io.realm.annotations.RealmModule +import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity import org.matrix.android.sdk.internal.database.model.RawCacheEntity /** @@ -24,6 +25,7 @@ import org.matrix.android.sdk.internal.database.model.RawCacheEntity */ @RealmModule(library = true, classes = [ - RawCacheEntity::class + RawCacheEntity::class, + KnownServerUrlEntity::class ]) internal class GlobalRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt index aee2a52818..a368dd77c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt @@ -24,7 +24,9 @@ import dagger.Module import dagger.Provides import io.realm.RealmConfiguration import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.internal.auth.DefaultHomeServerHistoryService import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.GlobalDatabase import org.matrix.android.sdk.internal.di.MatrixScope @@ -57,6 +59,8 @@ internal abstract class RawModule { realmKeysUtils.configureEncryption(this, DB_ALIAS) } .name("matrix-sdk-global.realm") + .schemaVersion(GlobalRealmMigration.SCHEMA_VERSION) + .migration(GlobalRealmMigration) .modules(GlobalRealmModule()) .build() } @@ -77,4 +81,7 @@ internal abstract class RawModule { @Binds abstract fun bindCleanRawCacheTask(task: DefaultCleanRawCacheTask): CleanRawCacheTask + + @Binds + abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 188ca32559..6b0048047d 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -64,7 +64,6 @@ import im.vector.app.features.login.LoginResetPasswordSuccessFragment import im.vector.app.features.login.LoginServerSelectionFragment import im.vector.app.features.login.LoginServerUrlFormFragment import im.vector.app.features.login.LoginSignUpSignInSelectionFragment -import im.vector.app.features.login.LoginSignUpSignInSsoFragment import im.vector.app.features.login.LoginSplashFragment import im.vector.app.features.login.LoginWaitForEmailFragment import im.vector.app.features.login.LoginWebFragment @@ -229,11 +228,11 @@ interface FragmentModule { @IntoMap @FragmentKey(LoginSignUpSignInSelectionFragment::class) fun bindLoginSignUpSignInSelectionFragment(fragment: LoginSignUpSignInSelectionFragment): Fragment - - @Binds - @IntoMap - @FragmentKey(LoginSignUpSignInSsoFragment::class) - fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment +// +// @Binds +// @IntoMap +// @FragmentKey(LoginSignUpSignInSsoFragment::class) +// fun bindLoginSignUpSignInSsoFragment(fragment: LoginSignUpSignInSsoFragment): Fragment @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 28f3a52efa..273a142ff1 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -59,6 +59,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import javax.inject.Singleton @@ -127,6 +128,8 @@ interface VectorComponent { fun rawService(): RawService + fun homeServerHistoryService(): HomeServerHistoryService + fun bugReporter(): BugReporter fun vectorUncaughtExceptionHandler(): VectorUncaughtExceptionHandler diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt index 1d7cd33241..77cad0ae73 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt @@ -33,6 +33,7 @@ import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session @@ -85,6 +86,12 @@ abstract class VectorModule { fun providesRawService(matrix: Matrix): RawService { return matrix.rawService() } + + @Provides + @JvmStatic + fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { + return matrix.homeServerHistoryService() + } } @Binds diff --git a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt index eb5aa86b3b..9480df4108 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt @@ -18,6 +18,7 @@ package im.vector.app.features.login import im.vector.app.core.platform.VectorViewModelAction import org.matrix.android.sdk.api.auth.data.Credentials +import org.matrix.android.sdk.api.auth.data.IdentityProvider import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.internal.network.ssl.Fingerprint @@ -60,7 +61,7 @@ sealed class LoginAction : VectorViewModelAction { object ResetResetPassword : ResetAction() // For the soft logout case - data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction() + data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String, val identityProvider: List?) : LoginAction() data class PostViewEvent(val viewEvent: LoginViewEvents) : LoginAction() diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 01e835b4e3..52efe128fb 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -112,7 +112,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { when (loginViewEvents) { - is LoginViewEvents.RegistrationFlowResult -> { + is LoginViewEvents.RegistrationFlowResult -> { // Check that all flows are supported by the application if (loginViewEvents.flowResult.missingStages.any { !it.isSupported() }) { // Display a popup to propose use web fallback @@ -133,7 +133,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc } } } - is LoginViewEvents.OutdatedHomeserver -> { + is LoginViewEvents.OutdatedHomeserver -> { AlertDialog.Builder(this) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_warning_content) @@ -141,7 +141,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc .show() Unit } - is LoginViewEvents.OpenServerSelection -> + is LoginViewEvents.OpenServerSelection -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, option = { ft -> @@ -153,28 +153,24 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) - is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents) - is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) - is LoginViewEvents.OnLoginFlowRetrieved -> + is LoginViewEvents.OnServerSelectionDone -> onServerSelectionDone(loginViewEvents) + is LoginViewEvents.OnSignModeSelected -> onSignModeSelected(loginViewEvents) + is LoginViewEvents.OnLoginFlowRetrieved -> addFragmentToBackstack(R.id.loginFragmentContainer, - if (loginViewEvents.isSso) { - LoginSignUpSignInSsoFragment::class.java - } else { - LoginSignUpSignInSelectionFragment::class.java - }, + LoginSignUpSignInSelectionFragment::class.java, option = commonOption) - is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) - is LoginViewEvents.OnForgetPasswordClicked -> + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordFragment::class.java, option = commonOption) - is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment::class.java, option = commonOption) } - is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) addFragmentToBackstack(R.id.loginFragmentContainer, LoginResetPasswordSuccessFragment::class.java, @@ -184,20 +180,20 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc // Go back to the login fragment supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } - is LoginViewEvents.OnSendEmailSuccess -> + is LoginViewEvents.OnSendEmailSuccess -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginWaitForEmailFragment::class.java, LoginWaitForEmailFragmentArgument(loginViewEvents.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is LoginViewEvents.OnSendMsisdnSuccess -> + is LoginViewEvents.OnSendMsisdnSuccess -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) is LoginViewEvents.Failure, - is LoginViewEvents.Loading -> + is LoginViewEvents.Loading -> // This is handled by the Fragments Unit }.exhaustive @@ -234,25 +230,26 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc when (loginViewEvents.serverType) { ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow ServerType.EMS, - ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, + ServerType.Other -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerUrlFormFragment::class.java, option = commonOption) - ServerType.Unknown -> Unit /* Should not happen */ + ServerType.Unknown -> Unit /* Should not happen */ } } private fun onSignModeSelected(loginViewEvents: LoginViewEvents.OnSignModeSelected) = withState(loginViewModel) { state -> // state.signMode could not be ready yet. So use value from the ViewEvent when (loginViewEvents.signMode) { - SignMode.Unknown -> error("Sign mode has to be set before calling this method") - SignMode.SignUp -> { + SignMode.Unknown -> error("Sign mode has to be set before calling this method") + SignMode.SignUp -> { // This is managed by the LoginViewEvents } - SignMode.SignIn -> { + SignMode.SignIn -> { // It depends on the LoginMode when (state.loginMode) { LoginMode.Unknown, - LoginMode.Sso -> error("Developer error") + is LoginMode.Sso -> error("Developer error") + is LoginMode.SsoAndPassword, LoginMode.Password -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginFragment::class.java, tag = FRAGMENT_LOGIN_TAG, @@ -331,17 +328,17 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc LoginCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, + is Stage.Email -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer, + is Stage.Msisdn -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginGenericTextInputFormFragment::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, + is Stage.Terms -> addFragmentToBackstack(R.id.loginFragmentContainer, LoginTermsFragment::class.java, LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, @@ -350,6 +347,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc } } + override fun onBackPressed() { + super.onBackPressed() + } + override fun configure(toolbar: Toolbar) { configureToolbar(toolbar) } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index 3ee0d6d9df..0ce4317deb 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -37,6 +37,7 @@ import io.reactivex.Observable import io.reactivex.functions.BiFunction import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.fragment_login.* +import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -79,15 +80,17 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { private fun setupAutoFill(state: LoginViewState) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> { + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_NEW_PASSWORD) + loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_UP } SignMode.SignIn, SignMode.SignInWithMatrixId -> { loginField.setAutofillHints(HintConstants.AUTOFILL_HINT_USERNAME) passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) + loginSocialLoginButtons.mode = SocialLoginButtonsView.Mode.MODE_SIGN_IN } }.exhaustive } @@ -142,9 +145,9 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginPasswordNotice.isVisible = true } else { val resId = when (state.signMode) { - SignMode.Unknown -> error("developer error") - SignMode.SignUp -> R.string.login_signup_to - SignMode.SignIn -> R.string.login_connect_to + SignMode.Unknown -> error("developer error") + SignMode.SignUp -> R.string.login_signup_to + SignMode.SignIn -> R.string.login_connect_to SignMode.SignInWithMatrixId -> R.string.login_connect_to } @@ -155,20 +158,28 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginNotice.text = getString(R.string.login_server_matrix_org_text) } - ServerType.EMS -> { + ServerType.EMS -> { loginServerIcon.isVisible = true loginServerIcon.setImageResource(R.drawable.ic_logo_element_matrix_services) loginTitle.text = getString(resId, "Element Matrix Services") loginNotice.text = getString(R.string.login_server_modular_text) } - ServerType.Other -> { + ServerType.Other -> { loginServerIcon.isVisible = false loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl()) loginNotice.text = getString(R.string.login_server_other_text) } - ServerType.Unknown -> Unit /* Should not happen */ + ServerType.Unknown -> Unit /* Should not happen */ } loginPasswordNotice.isVisible = false + + if (state.loginMode is LoginMode.SsoAndPassword) { + loginSocialLoginContainer.isVisible = true + loginSocialLoginButtons.identityProviders = state.loginMode.identityProviders + } else { + loginSocialLoginContainer.isVisible = false + loginSocialLoginButtons.identityProviders = null + } } } @@ -257,7 +268,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { passwordShown = false renderPasswordField() } - is Fail -> { + is Fail -> { val error = state.asyncLoginAction.error if (error is Failure.ServerError && error.error.code == MatrixError.M_FORBIDDEN diff --git a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt index 9a930dcd86..5121bf39ba 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginMode.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginMode.kt @@ -16,9 +16,31 @@ package im.vector.app.features.login -enum class LoginMode { - Unknown, - Password, - Sso, - Unsupported +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize +import org.matrix.android.sdk.api.auth.data.IdentityProvider + +sealed class LoginMode : Parcelable +/** because persist state */ { + @Parcelize object Unknown : LoginMode() + @Parcelize object Password : LoginMode() + @Parcelize data class Sso(val identityProviders: List?) : LoginMode() + @Parcelize data class SsoAndPassword(val identityProviders: List?) : LoginMode() + @Parcelize object Unsupported : LoginMode() +} + +fun LoginMode.ssoProviders() : List? { + return when (this) { + is LoginMode.Sso -> identityProviders + is LoginMode.SsoAndPassword -> identityProviders + else -> null + } +} + +fun LoginMode.hasSso() : Boolean { + return when (this) { + is LoginMode.Sso -> true + is LoginMode.SsoAndPassword -> true + else -> false + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt index f4595de634..c1854a3269 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerSelectionFragment.kt @@ -83,7 +83,7 @@ class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment if (state.loginMode != LoginMode.Unknown) { // LoginFlow for matrix.org has been retrieved - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode is LoginMode.Sso))) } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt index af959fecd4..1cd3148456 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt @@ -20,8 +20,10 @@ import android.annotation.SuppressLint import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo +import android.widget.ArrayAdapter import androidx.core.view.isVisible import butterknife.OnClick +import com.google.android.material.textfield.TextInputLayout import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard @@ -55,6 +57,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormHomeServerUrl.setOnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_DONE) { + loginServerUrlFormHomeServerUrl.dismissDropDown() submit() return@setOnEditorActionListener true } @@ -81,6 +84,13 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice) } } + val completions = state.knownCustomHomeServersUrls + loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter(requireContext(), android.R.layout.select_dialog_item, + completions + )) + loginServerUrlFormHomeServerUrlTil.endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU + .takeIf { completions.isNotEmpty() } + ?: TextInputLayout.END_ICON_NONE } @OnClick(R.id.loginServerUrlFormLearnMore) @@ -105,7 +115,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormHomeServerUrlTil.error = getString(R.string.login_error_invalid_home_server) } else -> { - loginServerUrlFormHomeServerUrl.setText(serverUrl) + loginServerUrlFormHomeServerUrl.setText(serverUrl, false /* to avoid completion dialog flicker*/) loginViewModel.handle(LoginAction.UpdateHomeServer(serverUrl)) } } @@ -131,7 +141,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() if (state.loginMode != LoginMode.Unknown) { // The home server url is valid - loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode == LoginMode.Sso))) + loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved(state.loginMode is LoginMode.Sso))) } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt index fa2f6b9df8..00b03835ae 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSelectionFragment.kt @@ -16,21 +16,36 @@ package im.vector.app.features.login +import android.content.ComponentName +import android.net.Uri +import androidx.browser.customtabs.CustomTabsClient +import androidx.browser.customtabs.CustomTabsServiceConnection +import androidx.browser.customtabs.CustomTabsSession import androidx.core.view.isVisible import butterknife.OnClick +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.extensions.toReducedUrl +import im.vector.app.core.utils.openUrlInChromeCustomTab import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* +import org.matrix.android.sdk.api.auth.data.IdentityProvider import javax.inject.Inject /** * In this screen, the user is asked to sign up or to sign in to the homeserver */ -open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { +class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() { + + // Map of sso urls by providers if any + private var ssoUrls = emptyMap().toMutableMap() + + private var customTabsServiceConnection: CustomTabsServiceConnection? = null + private var customTabsClient: CustomTabsClient? = null + private var customTabsSession: CustomTabsSession? = null override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection - protected fun setupUi(state: LoginViewState) { + private fun setupUi(state: LoginViewState) { when (state.serverType) { ServerType.MatrixOrg -> { loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org) @@ -51,16 +66,110 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo } ServerType.Unknown -> Unit /* Should not happen */ } + + val identityProviders = state.loginMode.ssoProviders() + if (state.loginMode.hasSso() && identityProviders.isNullOrEmpty().not()) { + loginSignupSigninSignInSocialLoginContainer.isVisible = true + loginSignupSigninSocialLoginButtons.identityProviders = identityProviders + loginSignupSigninSocialLoginButtons.listener = object: SocialLoginButtonsView.InteractionListener { + override fun onProviderSelected(id: IdentityProvider) { + ssoUrls[id.id]?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) } + } + } + } else { + loginSignupSigninSignInSocialLoginContainer.isVisible = false + loginSignupSigninSocialLoginButtons.identityProviders = null + } } - private fun setupButtons() { - loginSignupSigninSubmit.text = getString(R.string.login_signup) - loginSignupSigninSignIn.isVisible = true + override fun onStart() { + super.onStart() + val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() } + if (hasSSO) { + val packageName = CustomTabsClient.getPackageName(requireContext(), null) + + // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device + if (packageName != null) { + customTabsServiceConnection = object : CustomTabsServiceConnection() { + override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { + customTabsClient = client + .also { it.warmup(0L) } + + // prefetch urls + prefetchSsoUrls() + } + + override fun onServiceDisconnected(name: ComponentName?) { + } + } + .also { + CustomTabsClient.bindCustomTabsService( + requireContext(), + // Despite the API, packageName cannot be null + packageName, + it + ) + } + } + } + } + + override fun onStop() { + super.onStop() + val hasSSO = withState(loginViewModel) { it.loginMode.hasSso() } + if (hasSSO) { + customTabsServiceConnection?.let { requireContext().unbindService(it) } + customTabsServiceConnection = null + } + } + + private fun prefetchSsoUrls() = withState(loginViewModel) { state -> + val providers = state.loginMode.ssoProviders() + if (providers.isNullOrEmpty()) { + state.getSsoUrl(null).let { + ssoUrls[null] = it + prefetchUrl(it) + } + } else { + providers.forEach { identityProvider -> + state.getSsoUrl(identityProvider.id).let { + ssoUrls[identityProvider.id] = it + // we don't prefetch for privacy reasons + } + } + } + } + + private fun prefetchUrl(url: String) { + if (customTabsSession == null) { + customTabsSession = customTabsClient?.newSession(null) + } + + customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) + } + + private fun setupButtons(state: LoginViewState) { + when (state.loginMode) { + is LoginMode.Sso -> { + // change to only one button that is sign in with sso + loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) + loginSignupSigninSignIn.isVisible = false + } + else -> { + loginSignupSigninSubmit.text = getString(R.string.login_signup) + loginSignupSigninSignIn.isVisible = true + } + } } @OnClick(R.id.loginSignupSigninSubmit) - open fun submit() { - loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + fun submit() = withState(loginViewModel) { state -> + if (state.loginMode is LoginMode.Sso) { + ssoUrls[null]?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) } + } else { + loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) + } + Unit } @OnClick(R.id.loginSignupSigninSignIn) @@ -74,6 +183,6 @@ open class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLo override fun updateWithState(state: LoginViewState) { setupUi(state) - setupButtons() + setupButtons(state) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt deleted file mode 100644 index 8d33ec2e27..0000000000 --- a/vector/src/main/java/im/vector/app/features/login/LoginSignUpSignInSsoFragment.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2020 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 - -import android.content.ComponentName -import android.net.Uri -import androidx.browser.customtabs.CustomTabsClient -import androidx.browser.customtabs.CustomTabsServiceConnection -import androidx.browser.customtabs.CustomTabsSession -import androidx.core.view.isVisible -import im.vector.app.R -import im.vector.app.core.utils.openUrlInChromeCustomTab -import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.* -import javax.inject.Inject - -/** - * In this screen, the user is asked to sign up or to sign in using SSO - * This Fragment binds a CustomTabsServiceConnection if available, then prefetch the SSO url, as it will be likely to be opened. - */ -open class LoginSignUpSignInSsoFragment @Inject constructor() : LoginSignUpSignInSelectionFragment() { - - private var ssoUrl: String? = null - private var customTabsServiceConnection: CustomTabsServiceConnection? = null - private var customTabsClient: CustomTabsClient? = null - private var customTabsSession: CustomTabsSession? = null - - override fun onStart() { - super.onStart() - - val packageName = CustomTabsClient.getPackageName(requireContext(), null) - - // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device - if (packageName != null) { - customTabsServiceConnection = object : CustomTabsServiceConnection() { - override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { - customTabsClient = client - .also { it.warmup(0L) } - } - - override fun onServiceDisconnected(name: ComponentName?) { - } - } - .also { - CustomTabsClient.bindCustomTabsService( - requireContext(), - // Despite the API, packageName cannot be null - packageName, - it - ) - } - } - } - - private fun prefetchUrl(url: String) { - if (ssoUrl != null) return - - ssoUrl = url - if (customTabsSession == null) { - customTabsSession = customTabsClient?.newSession(null) - } - - customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null) - } - - override fun onStop() { - super.onStop() - customTabsServiceConnection?.let { requireContext().unbindService(it) } - customTabsServiceConnection = null - } - - private fun setupButtons() { - loginSignupSigninSubmit.text = getString(R.string.login_signin_sso) - loginSignupSigninSignIn.isVisible = false - } - - override fun submit() { - ssoUrl?.let { openUrlInChromeCustomTab(requireContext(), customTabsSession, it) } - } - - override fun updateWithState(state: LoginViewState) { - setupUi(state) - setupButtons() - prefetchUrl(state.getSsoUrl()) - } -} diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 1f47916538..e00e4065e1 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -28,6 +28,7 @@ import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart @@ -38,6 +39,7 @@ import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.features.signout.soft.SoftLogoutActivity import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.AuthenticationService +import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowTypes @@ -63,7 +65,8 @@ class LoginViewModel @AssistedInject constructor( private val activeSessionHolder: ActiveSessionHolder, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, private val reAuthHelper: ReAuthHelper, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val homeServerHistoryService: HomeServerHistoryService ) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -71,12 +74,21 @@ class LoginViewModel @AssistedInject constructor( fun create(initialState: LoginViewState): LoginViewModel } + init { + if (BuildConfig.DEBUG) { + homeServerHistoryService.addHomeServerToHistory("http://10.0.2.2:8080") + } + setState { + copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls()) + } + } + companion object : MvRxViewModelFactory { @JvmStatic override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? { return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) { - is LoginActivity -> activity.loginViewModelFactory.create(state) + is LoginActivity -> activity.loginViewModelFactory.create(state) is SoftLogoutActivity -> activity.loginViewModelFactory.create(state) else -> error("Invalid Activity") } @@ -108,20 +120,20 @@ class LoginViewModel @AssistedInject constructor( override fun handle(action: LoginAction) { when (action) { - is LoginAction.UpdateServerType -> handleUpdateServerType(action) - is LoginAction.UpdateSignMode -> handleUpdateSignMode(action) - is LoginAction.InitWith -> handleInitWith(action) - is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } - is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } - is LoginAction.LoginWithToken -> handleLoginWithToken(action) - is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) - is LoginAction.ResetPassword -> handleResetPassword(action) + is LoginAction.UpdateServerType -> handleUpdateServerType(action) + is LoginAction.UpdateSignMode -> handleUpdateSignMode(action) + is LoginAction.InitWith -> handleInitWith(action) + is LoginAction.UpdateHomeServer -> handleUpdateHomeserver(action).also { lastAction = action } + is LoginAction.LoginOrRegister -> handleLoginOrRegister(action).also { lastAction = action } + is LoginAction.LoginWithToken -> handleLoginWithToken(action) + is LoginAction.WebLoginSuccess -> handleWebLoginSuccess(action) + is LoginAction.ResetPassword -> handleResetPassword(action) is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed() - is LoginAction.RegisterAction -> handleRegisterAction(action) - is LoginAction.ResetAction -> handleResetAction(action) + is LoginAction.RegisterAction -> handleRegisterAction(action) + is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) - is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) - is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) + is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) }.exhaustive } @@ -129,11 +141,13 @@ class LoginViewModel @AssistedInject constructor( // It happen when we get the login flow, or during direct authentication. // So alter the homeserver config and retrieve again the login flow when (val finalLastAction = lastAction) { - is LoginAction.UpdateHomeServer -> + is LoginAction.UpdateHomeServer -> { + rememberHomeServer(finalLastAction.homeServerUrl) currentHomeServerConnectionConfig ?.let { it.copy(allowedFingerprints = it.allowedFingerprints + action.fingerprint) } ?.let { getLoginFlow(it) } - is LoginAction.LoginOrRegister -> + } + is LoginAction.LoginOrRegister -> handleDirectLogin( finalLastAction, HomeServerConnectionConfig.Builder() @@ -145,6 +159,13 @@ class LoginViewModel @AssistedInject constructor( } } + private fun rememberHomeServer(homeServerUrl: String) { + homeServerHistoryService.addHomeServerToHistory(homeServerUrl) + setState { + copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls()) + } + } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { val safeLoginWizard = loginWizard @@ -184,7 +205,7 @@ class LoginViewModel @AssistedInject constructor( setState { copy( signMode = SignMode.SignIn, - loginMode = LoginMode.Sso, + loginMode = LoginMode.Sso(action.identityProvider), homeServerUrl = action.homeServerUrl, deviceId = action.deviceId ) @@ -193,14 +214,14 @@ class LoginViewModel @AssistedInject constructor( private fun handleRegisterAction(action: LoginAction.RegisterAction) { when (action) { - is LoginAction.CaptchaDone -> handleCaptchaDone(action) - is LoginAction.AcceptTerms -> handleAcceptTerms() - is LoginAction.RegisterDummy -> handleRegisterDummy() - is LoginAction.AddThreePid -> handleAddThreePid(action) - is LoginAction.SendAgainThreePid -> handleSendAgainThreePid() - is LoginAction.ValidateThreePid -> handleValidateThreePid(action) + is LoginAction.CaptchaDone -> handleCaptchaDone(action) + is LoginAction.AcceptTerms -> handleAcceptTerms() + is LoginAction.RegisterDummy -> handleRegisterDummy() + is LoginAction.AddThreePid -> handleAddThreePid(action) + is LoginAction.SendAgainThreePid -> handleSendAgainThreePid() + is LoginAction.ValidateThreePid -> handleValidateThreePid(action) is LoginAction.CheckIfEmailHasBeenValidated -> handleCheckIfEmailHasBeenValidated(action) - is LoginAction.StopEmailValidationCheck -> handleStopEmailValidationCheck() + is LoginAction.StopEmailValidationCheck -> handleStopEmailValidationCheck() } } @@ -237,7 +258,7 @@ class LoginViewModel @AssistedInject constructor( } when (data) { - is RegistrationResult.Success -> onSessionCreated(data.session) + is RegistrationResult.Success -> onSessionCreated(data.session) is RegistrationResult.FlowResponse -> onFlowResponse(data.flowResult) } } @@ -337,7 +358,7 @@ class LoginViewModel @AssistedInject constructor( ) } } - LoginAction.ResetHomeServerUrl -> { + LoginAction.ResetHomeServerUrl -> { authenticationService.reset() setState { @@ -350,7 +371,7 @@ class LoginViewModel @AssistedInject constructor( ) } } - LoginAction.ResetSignMode -> { + LoginAction.ResetSignMode -> { setState { copy( asyncHomeServerLoginFlowRequest = Uninitialized, @@ -360,7 +381,7 @@ class LoginViewModel @AssistedInject constructor( ) } } - LoginAction.ResetLogin -> { + LoginAction.ResetLogin -> { authenticationService.cancelPendingLoginOrRegistration() setState { @@ -370,7 +391,7 @@ class LoginViewModel @AssistedInject constructor( ) } } - LoginAction.ResetResetPassword -> { + LoginAction.ResetResetPassword -> { setState { copy( asyncResetPassword = Uninitialized, @@ -390,10 +411,10 @@ class LoginViewModel @AssistedInject constructor( } when (action.signMode) { - SignMode.SignUp -> startRegistrationFlow() - SignMode.SignIn -> startAuthenticationFlow() + SignMode.SignUp -> startRegistrationFlow() + SignMode.SignIn -> startAuthenticationFlow() SignMode.SignInWithMatrixId -> _viewEvents.post(LoginViewEvents.OnSignModeSelected(SignMode.SignInWithMatrixId)) - SignMode.Unknown -> Unit + SignMode.Unknown -> Unit } } @@ -405,12 +426,12 @@ class LoginViewModel @AssistedInject constructor( } when (action.serverType) { - ServerType.Unknown -> Unit /* Should not happen */ + ServerType.Unknown -> Unit /* Should not happen */ ServerType.MatrixOrg -> // Request login flow here handle(LoginAction.UpdateHomeServer(matrixOrgUrl)) ServerType.EMS, - ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType)) + ServerType.Other -> _viewEvents.post(LoginViewEvents.OnServerSelectionDone(action.serverType)) }.exhaustive } @@ -514,9 +535,9 @@ class LoginViewModel @AssistedInject constructor( private fun handleLoginOrRegister(action: LoginAction.LoginOrRegister) = withState { state -> when (state.signMode) { - SignMode.Unknown -> error("Developer error, invalid sign mode") - SignMode.SignIn -> handleLogin(action) - SignMode.SignUp -> handleRegisterWith(action) + SignMode.Unknown -> error("Developer error, invalid sign mode") + SignMode.SignIn -> handleLogin(action) + SignMode.SignUp -> handleRegisterWith(action) SignMode.SignInWithMatrixId -> handleDirectLogin(action, null) }.exhaustive } @@ -713,11 +734,11 @@ class LoginViewModel @AssistedInject constructor( private fun handleUpdateHomeserver(action: LoginAction.UpdateHomeServer) { val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) - if (homeServerConnectionConfig == null) { // This is invalid _viewEvents.post(LoginViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { + rememberHomeServer(action.homeServerUrl) getLoginFlow(homeServerConnectionConfig) } } @@ -755,9 +776,11 @@ class LoginViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } // FIXME We should post a view event here normally? diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt index 194ed668bc..383fd4a54e 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt @@ -51,7 +51,8 @@ data class LoginViewState( val loginMode: LoginMode = LoginMode.Unknown, @PersistState // Supported types for the login. We cannot use a sealed class for LoginType because it is not serializable - val loginModeSupportedTypes: List = emptyList() + val loginModeSupportedTypes: List = emptyList(), + val knownCustomHomeServersUrls: List = emptyList() ) : MvRxState { fun isLoading(): Boolean { @@ -68,10 +69,13 @@ data class LoginViewState( return asyncLoginAction is Success } - fun getSsoUrl(): String { + fun getSsoUrl(providerId: String?): String { return buildString { append(homeServerUrl?.trim { it == '/' }) append(SSO_REDIRECT_PATH) + if (providerId != null) { + append("/$providerId") + } // Set a redirect url we will intercept later appendParamToUrl(SSO_REDIRECT_URL_PARAM, VECTOR_REDIRECT_URL) deviceId?.takeIf { it.isNotBlank() }?.let { diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt new file mode 100644 index 0000000000..1d323c0fbc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2020 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 + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.children +import com.google.android.material.button.MaterialButton +import im.vector.app.R +import org.matrix.android.sdk.api.auth.data.IdentityProvider + +class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) + : LinearLayout(context, attrs, defStyle) { + + interface InteractionListener { + fun onProviderSelected(id: IdentityProvider) + } + + enum class Mode { + MODE_SIGN_IN, + MODE_SIGN_UP, + MODE_CONTINUE, + } + + var identityProviders: List? = null + set(newProviders) { + if (newProviders != identityProviders) { + field = newProviders + update() + } + } + + var mode: Mode = Mode.MODE_CONTINUE + set(value) { + if (value != mode) { + field = value + update() + } + } + + var listener: InteractionListener? = null + + private fun update() { + val cachedViews = emptyMap().toMutableMap() + children.filterIsInstance().forEach { + cachedViews[it.getTag(R.id.loginSignupSigninSocialLoginButtons)?.toString() ?: ""] = it + } + removeAllViews() + if (identityProviders.isNullOrEmpty()) { + return + } + + identityProviders?.forEach { identityProvider -> + // Use some heuristic to render buttons according to branding guidelines + val cached = cachedViews[identityProvider.id] + val button: MaterialButton = if (cached != null) { + cached + } else { + when (identityProvider.id) { + "google" -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_google_style) + } + "github" -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_github_style) + } + "apple" -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style) + } + "facebook" -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style) + } + "twitter" -> { + MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style) + } + else -> { + MaterialButton(context, null, R.attr.materialButtonStyle).apply { + transformationMethod = null + textAlignment = View.TEXT_ALIGNMENT_CENTER + } + } + } + } + button.text = getButtonTitle(identityProvider) + button.setTag(R.id.loginSignupSigninSocialLoginButtons, identityProvider.id) + button.setOnClickListener { + listener?.onProviderSelected(identityProvider) + } + addView(button) + } + } + + private fun getButtonTitle(provider: IdentityProvider): String { + return when (mode) { + Mode.MODE_SIGN_IN -> context.getString(R.string.login_social_signin_with, provider.name) + Mode.MODE_SIGN_UP -> context.getString(R.string.login_social_signup_with, provider.name) + Mode.MODE_CONTINUE -> context.getString(R.string.login_social_continue_with, provider.name) + } + } + + init { + this.orientation = VERTICAL + gravity = Gravity.CENTER + clipToPadding = false + clipChildren = false + @SuppressLint("SetTextI18n") + if (isInEditMode) { + identityProviders = listOf( + IdentityProvider("google", "Google", null), + IdentityProvider("facebook", "Facebook", null), + IdentityProvider("apple", "Apple", null), + IdentityProvider("github", "Github", null), + IdentityProvider("twitter", "Twitter", null), + IdentityProvider("Custom_pro", "SSO", null) + ) + } + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0) + val modeAttr = typedArray.getInt(R.styleable.SocialLoginButtonsView_signMode, 2) + mode = when (modeAttr) { + 0 -> Mode.MODE_SIGN_IN + 1 -> Mode.MODE_SIGN_UP + else -> Mode.MODE_CONTINUE + } + typedArray.recycle() + } + + fun dpToPx(dp: Int): Int { + val resources = context.resources + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt() + } +} diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 989fc0aadb..89fa4a982a 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -107,7 +107,7 @@ class SoftLogoutController @Inject constructor( } is Success -> { when (state.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Password -> { + LoginMode.Password -> { loginPasswordFormItem { id("passwordForm") stringProvider(stringProvider) @@ -120,21 +120,23 @@ class SoftLogoutController @Inject constructor( submitClickListener { password -> listener?.signinSubmit(password) } } } - LoginMode.Sso -> { + is LoginMode.Sso -> { loginCenterButtonItem { id("sso") text(stringProvider.getString(R.string.login_signin_sso)) listener { listener?.signinFallbackSubmit() } } } - LoginMode.Unsupported -> { + is LoginMode.SsoAndPassword -> { + } + LoginMode.Unsupported -> { loginCenterButtonItem { id("fallback") text(stringProvider.getString(R.string.login_signin)) listener { listener?.signinFallbackSubmit() } } } - LoginMode.Unknown -> Unit // Should not happen + LoginMode.Unknown -> Unit // Should not happen } } } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt index dbd5028401..0fc1076f1d 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutFragment.kt @@ -54,14 +54,27 @@ class SoftLogoutFragment @Inject constructor( softLogoutViewModel.subscribe(this) { softLogoutViewState -> softLogoutController.update(softLogoutViewState) - - when (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Sso, + when (val mode = softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) { + is LoginMode.SsoAndPassword -> { + loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + mode.identityProviders + )) + } + is LoginMode.Sso -> { + loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( + softLogoutViewState.homeServerUrl, + softLogoutViewState.deviceId, + mode.identityProviders + )) + } LoginMode.Unsupported -> { // Prepare the loginViewModel for a SSO/login fallback recovery loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery( softLogoutViewState.homeServerUrl, - softLogoutViewState.deviceId + softLogoutViewState.deviceId, + null )) } else -> Unit diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt index 4f6110aab1..f1d9a66342 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutViewModel.kt @@ -105,9 +105,11 @@ class SoftLogoutViewModel @AssistedInject constructor( is LoginFlowResult.Success -> { val loginMode = when { // SSO login is taken first - data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso - data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password - else -> LoginMode.Unsupported + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) + && data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(data.ssoIdentityProviders) + data.supportedLoginTypes.contains(LoginFlowTypes.PASSWORD) -> LoginMode.Password + else -> LoginMode.Unsupported } setState { diff --git a/vector/src/main/res/color/button_social_google_background_selector_dark.xml b/vector/src/main/res/color/button_social_google_background_selector_dark.xml new file mode 100644 index 0000000000..1369414e58 --- /dev/null +++ b/vector/src/main/res/color/button_social_google_background_selector_dark.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/color/button_social_google_background_selector_light.xml b/vector/src/main/res/color/button_social_google_background_selector_light.xml new file mode 100644 index 0000000000..bcac13885b --- /dev/null +++ b/vector/src/main/res/color/button_social_google_background_selector_light.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_social_apple.xml b/vector/src/main/res/drawable/ic_social_apple.xml new file mode 100644 index 0000000000..5d745a12a3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_apple.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_facebook.xml b/vector/src/main/res/drawable/ic_social_facebook.xml new file mode 100644 index 0000000000..77cb8be760 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_facebook.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_github.xml b/vector/src/main/res/drawable/ic_social_github.xml new file mode 100644 index 0000000000..602f16aa60 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_github.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/drawable/ic_social_google.xml b/vector/src/main/res/drawable/ic_social_google.xml new file mode 100644 index 0000000000..1518c4cb29 --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_google.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_social_twitter.xml b/vector/src/main/res/drawable/ic_social_twitter.xml new file mode 100644 index 0000000000..7139c3c9bc --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_twitter.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/layout/fragment_login.xml b/vector/src/main/res/layout/fragment_login.xml index ca7d267c80..2559e6d177 100644 --- a/vector/src/main/res/layout/fragment_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -19,9 +19,9 @@ android:id="@+id/loginServerIcon" android:layout_width="wrap_content" android:layout_height="wrap_content" - tools:src="@drawable/ic_logo_matrix_org" app:tint="?riotx_text_primary" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_logo_matrix_org" /> @@ -136,6 +136,37 @@ + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index 37122e8e43..3c223f19cc 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -53,14 +53,14 @@ - + + + + + + + + + diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 55a4f9a84e..b97a8f3837 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -41,6 +41,12 @@ + + + + + + @@ -66,4 +72,12 @@ + + + + + + + + diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index 647ada65be..957d30398b 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -41,6 +41,7 @@ #FF000000 #FFFFFFFF #55000000 + #8A000000 Connect to %1$s diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 2dff0ab39c..b84dc1e138 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -370,5 +370,103 @@ @drawable/vector_tabbar_background @drawable/vector_tabbar_background + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 6ebf8e2b9b..6be0adb907 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -194,6 +194,12 @@ @transition/image_preview_transition @transition/image_preview_transition + + @style/WidgetButtonSocialLogin.Google.Dark + @style/WidgetButtonSocialLogin.Github.Dark + @style/WidgetButtonSocialLogin.Facebook.Dark + @style/WidgetButtonSocialLogin.Twitter.Dark + @style/WidgetButtonSocialLogin.Apple.Dark - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/values/styles_social_login.xml b/vector/src/main/res/values/styles_social_login.xml new file mode 100644 index 0000000000..796965cee1 --- /dev/null +++ b/vector/src/main/res/values/styles_social_login.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 09040b70954efc20e5b28e80af9939945bca2bfa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 14 Dec 2020 16:20:36 +0100 Subject: [PATCH 20/20] Clear history (#1933) --- CHANGES.md | 1 + .../org/matrix/android/sdk/internal/raw/RawModule.kt | 3 +-- .../java/im/vector/app/features/login/LoginAction.kt | 3 +++ .../app/features/login/LoginServerUrlFormFragment.kt | 11 ++++++++++- .../im/vector/app/features/login/LoginViewModel.kt | 12 +++++++----- .../res/layout/fragment_login_server_url_form.xml | 11 +++++++++++ vector/src/main/res/values/strings.xml | 1 + 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 78cea417aa..62bd92006e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Improvements 🙌: - Add Setting Item to Change PIN (#2462) - Improve room history visibility setting UX (#1579) - Matrix.to deeplink custom scheme support + - Homeserver history (#1933) Bugfix 🐛: - Fix cancellation of sending event (#2438) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt index 6ed14ddb3c..50721b809a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/RawModule.kt @@ -24,9 +24,7 @@ import dagger.Module import dagger.Provides import io.realm.RealmConfiguration import okhttp3.OkHttpClient -import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.raw.RawService -import org.matrix.android.sdk.internal.auth.DefaultHomeServerHistoryService import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.GlobalDatabase import org.matrix.android.sdk.internal.di.MatrixScope @@ -61,6 +59,7 @@ internal abstract class RawModule { .name("matrix-sdk-global.realm") .schemaVersion(GlobalRealmMigration.SCHEMA_VERSION) .migration(GlobalRealmMigration) + .allowWritesOnUiThread(true) .modules(GlobalRealmModule()) .build() } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt index 9ab02711b5..2b4e3d6be0 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginAction.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginAction.kt @@ -60,6 +60,9 @@ sealed class LoginAction : VectorViewModelAction { object ResetLogin : ResetAction() object ResetResetPassword : ResetAction() + // Homeserver history + object ClearHomeServerHistory : LoginAction() + // For the soft logout case data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String, diff --git a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt index 716c1cda88..1915cdd204 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginServerUrlFormFragment.kt @@ -21,10 +21,12 @@ import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter +import androidx.core.view.isInvisible import androidx.core.view.isVisible import butterknife.OnClick import com.google.android.material.textfield.TextInputLayout import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.utils.ensureProtocol @@ -84,7 +86,7 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() loginServerUrlFormNotice.text = getString(R.string.login_server_url_form_common_notice) } } - val completions = state.knownCustomHomeServersUrls + val completions = state.knownCustomHomeServersUrls + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList() loginServerUrlFormHomeServerUrl.setAdapter(ArrayAdapter( requireContext(), R.layout.item_completion_homeserver, @@ -100,6 +102,11 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() openUrlInChromeCustomTab(requireActivity(), null, EMS_LINK) } + @OnClick(R.id.loginServerUrlFormClearHistory) + fun clearHistory() { + loginViewModel.handle(LoginAction.ClearHomeServerHistory) + } + override fun resetViewModel() { loginViewModel.handle(LoginAction.ResetHomeServerUrl) } @@ -141,6 +148,8 @@ class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() override fun updateWithState(state: LoginViewState) { setupUi(state) + loginServerUrlFormClearHistory.isInvisible = state.knownCustomHomeServersUrls.isEmpty() + if (state.loginMode != LoginMode.Unknown) { // The home server url is valid loginViewModel.handle(LoginAction.PostViewEvent(LoginViewEvents.OnLoginFlowRetrieved)) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 2f84c3081d..0a6dbcaae2 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -28,7 +28,6 @@ import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.configureAndStart @@ -80,10 +79,7 @@ class LoginViewModel @AssistedInject constructor( private fun getKnownCustomHomeServersUrls() { setState { - copy( - knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls() - + if (BuildConfig.DEBUG) listOf("http://10.0.2.2:8080") else emptyList() - ) + copy(knownCustomHomeServersUrls = homeServerHistoryService.getKnownServersUrls()) } } @@ -137,6 +133,7 @@ class LoginViewModel @AssistedInject constructor( is LoginAction.ResetAction -> handleResetAction(action) is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action) is LoginAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) + LoginAction.ClearHomeServerHistory -> handleClearHomeServerHistory() is LoginAction.PostViewEvent -> _viewEvents.post(action.viewEvent) }.exhaustive } @@ -167,6 +164,11 @@ class LoginViewModel @AssistedInject constructor( getKnownCustomHomeServersUrls() } + private fun handleClearHomeServerHistory() { + homeServerHistoryService.clearHistory() + getKnownCustomHomeServersUrls() + } + private fun handleLoginWithToken(action: LoginAction.LoginWithToken) { val safeLoginWizard = loginWizard diff --git a/vector/src/main/res/layout/fragment_login_server_url_form.xml b/vector/src/main/res/layout/fragment_login_server_url_form.xml index 3c223f19cc..a441fee3be 100644 --- a/vector/src/main/res/layout/fragment_login_server_url_form.xml +++ b/vector/src/main/res/layout/fragment_login_server_url_form.xml @@ -70,6 +70,17 @@ + + Sign Up Sign In Continue with SSO + Clear history Element Matrix Services Address Address