From a3ad8c5e2e5dc4c886540f09e645b6bb10668020 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 11:29:26 +0000 Subject: [PATCH 1/5] removing non accessible softlogout2 activity - there's no manifest entry - the implementation looks like it delegates back to login 1, will look to add back as part of the FTUE changes --- .../features/navigation/DefaultNavigator.kt | 6 +- .../signout/soft/SoftLogoutActivity2.kt | 113 ------------------ 2 files changed, 1 insertion(+), 118 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt 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 eacd8523cf..befc62d2ce 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 @@ -80,7 +80,6 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.share.SharedData import im.vector.app.features.signout.soft.SoftLogoutActivity -import im.vector.app.features.signout.soft.SoftLogoutActivity2 import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpacePreviewActivity @@ -128,10 +127,7 @@ class DefaultNavigator @Inject constructor( } override fun softLogout(context: Context) { - val intent = when (features.loginVersion()) { - VectorFeatures.LoginVersion.V1 -> SoftLogoutActivity.newIntent(context) - VectorFeatures.LoginVersion.V2 -> SoftLogoutActivity2.newIntent(context) - } + val intent = SoftLogoutActivity.newIntent(context) context.startActivity(intent) } diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt deleted file mode 100644 index 8489b2baef..0000000000 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutActivity2.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2021 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.signout.soft - -import android.content.Context -import android.content.Intent -import androidx.core.view.isVisible -import androidx.fragment.app.FragmentManager -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.viewModel -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.R -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.replaceFragment -import im.vector.app.features.MainActivity -import im.vector.app.features.MainActivityArgs -import im.vector.app.features.login2.LoginActivity2 -import org.matrix.android.sdk.api.failure.GlobalError -import org.matrix.android.sdk.api.session.Session -import timber.log.Timber -import javax.inject.Inject - -/** - * In this screen, the user is viewing a message informing that he has been logged out - * Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free - * - * This is just a copy of SoftLogoutActivity2, which extends LoginActivity2 - */ -@AndroidEntryPoint -class SoftLogoutActivity2 : LoginActivity2() { - - private val softLogoutViewModel: SoftLogoutViewModel by viewModel() - - @Inject lateinit var session: Session - @Inject lateinit var errorFormatter: ErrorFormatter - - override fun initUiAndData() { - super.initUiAndData() - - softLogoutViewModel.onEach { - updateWithState(it) - } - - softLogoutViewModel.observeViewEvents { handleSoftLogoutViewEvents(it) } - } - - private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) { - when (softLogoutViewEvents) { - is SoftLogoutViewEvents.Failure -> - showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable)) - is SoftLogoutViewEvents.ErrorNotSameUser -> { - // Pop the backstack - supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - - // And inform the user - showError(getString( - R.string.soft_logout_sso_not_same_user_error, - softLogoutViewEvents.currentUserId, - softLogoutViewEvents.newUserId) - ) - } - is SoftLogoutViewEvents.ClearData -> { - MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true)) - } - } - } - - private fun showError(message: String) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.dialog_title_error) - .setMessage(message) - .setPositiveButton(R.string.ok, null) - .show() - } - - override fun addFirstFragment() { - replaceFragment(views.loginFragmentContainer, SoftLogoutFragment::class.java) - } - - private fun updateWithState(softLogoutViewState: SoftLogoutViewState) { - if (softLogoutViewState.asyncLoginAction is Success) { - MainActivity.restartApp(this, MainActivityArgs()) - } - - views.loginLoading.isVisible = softLogoutViewState.isLoading() - } - - companion object { - fun newIntent(context: Context): Intent { - return Intent(context, SoftLogoutActivity2::class.java) - } - } - - override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { - // No op here - Timber.w("Ignoring invalid token global error") - } -} From 01d4a48b8b71820517ece88abb49969dbb7a6274 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 11:31:01 +0000 Subject: [PATCH 2/5] adding ability to lazily create viewmodels - helpful when multiple view models are injected but not all are needed --- .../app/core/extensions/MavericksViewModel.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt b/vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt new file mode 100644 index 0000000000..6120a84d7c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/MavericksViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 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.core.extensions + +import androidx.activity.ComponentActivity +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Mavericks +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelProvider + +inline fun , reified S : MavericksState> ComponentActivity.lazyViewModel(): Lazy { + return lazy(mode = LazyThreadSafetyMode.NONE) { + MavericksViewModelProvider.get( + viewModelClass = VM::class.java, + stateClass = S::class.java, + viewModelContext = ActivityViewModelContext(this, intent.extras?.get(Mavericks.KEY_ARG)), + key = VM::class.java.name + ) + } +} From dae2e9988ffda803fcec0853f3f690bd4ae523f9 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 11:33:35 +0000 Subject: [PATCH 3/5] moving the sso redirect parameter to the sso redirect activity for sharing --- .../im/vector/app/features/login/AbstractSSOLoginFragment.kt | 2 +- .../main/java/im/vector/app/features/login/LoginActivity.kt | 3 --- .../main/java/im/vector/app/features/login/LoginFragment.kt | 2 +- .../app/features/login/LoginSignUpSignInSelectionFragment.kt | 4 ++-- .../vector/app/features/login/SSORedirectRouterActivity.kt | 5 +++++ .../vector/app/features/login2/AbstractSSOLoginFragment2.kt | 3 ++- .../app/features/login2/LoginFragmentSignupUsername2.kt | 3 ++- .../im/vector/app/features/login2/LoginFragmentToAny2.kt | 3 ++- .../im/vector/app/features/login2/LoginSsoOnlyFragment2.kt | 3 ++- 9 files changed, 17 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index 8663b7c73f..b18df6c9cf 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -88,7 +88,7 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragmen if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) loginViewModel.getSsoUrl( - redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) 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 c46dca27b3..5ab08ffff7 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 @@ -356,9 +356,6 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo private const val EXTRA_CONFIG = "EXTRA_CONFIG" - // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string - const val VECTOR_REDIRECT_URL = "element://connect" - fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { return Intent(context, LoginActivity::class.java).apply { putExtra(EXTRA_CONFIG, loginConfig) 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 9ca8a1dbec..da61d95997 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 @@ -200,7 +200,7 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment if (state.loginMode is LoginMode.Sso) { loginViewModel.getSsoUrl( - redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) diff --git a/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt b/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt index 29f8559362..19c549fd45 100644 --- a/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/SSORedirectRouterActivity.kt @@ -32,4 +32,9 @@ class SSORedirectRouterActivity : AppCompatActivity() { navigator.loginSSORedirect(this, intent.data) finish() } + + companion object { + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + const val VECTOR_REDIRECT_URL = "element://connect" + } } diff --git a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt index 43f301d9b4..8bc531b25d 100644 --- a/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/AbstractSSOLoginFragment2.kt @@ -24,6 +24,7 @@ import androidx.browser.customtabs.CustomTabsSession import androidx.viewbinding.ViewBinding import com.airbnb.mvrx.withState import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.hasSso import im.vector.app.features.login.ssoIdentityProviders @@ -90,7 +91,7 @@ abstract class AbstractSSOLoginFragment2 : AbstractLoginFragme if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) loginViewModel.getSsoUrl( - redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt index 51044ac153..f9917a4c31 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentSignupUsername2.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSignupUsername2Binding import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map @@ -97,7 +98,7 @@ class LoginFragmentSignupUsername2 @Inject constructor() : AbstractSSOLoginFragm views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { override fun onProviderSelected(id: String?) { loginViewModel.getSsoUrl( - redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = id ) diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt index 48792da007..3fa0e6c549 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt +++ b/vector/src/main/java/im/vector/app/features/login2/LoginFragmentToAny2.kt @@ -31,6 +31,7 @@ import im.vector.app.core.extensions.hidePassword import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentLoginSigninToAny2Binding import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.SSORedirectRouterActivity import im.vector.app.features.login.SocialLoginButtonsView import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -124,7 +125,7 @@ class LoginFragmentToAny2 @Inject constructor() : AbstractSSOLoginFragment2 loginViewModel.getSsoUrl( - redirectUrl = LoginActivity2.VECTOR_REDIRECT_URL, + redirectUrl = SSORedirectRouterActivity.VECTOR_REDIRECT_URL, deviceId = state.deviceId, providerId = null ) From fd0e1e44c4388d16d492bf146f26b60365c31d8b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 11:54:21 +0000 Subject: [PATCH 4/5] renaming login version to variants to better describe the different flows --- vector/build.gradle | 2 +- .../debug/features/DebugFeaturesStateFactory.kt | 4 ++-- .../features/debug/features/DebugVectorFeatures.kt | 4 ++-- .../java/im/vector/app/features/VectorFeatures.kt | 10 +++++----- .../app/features/navigation/DefaultNavigator.kt | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index 9f6c91c2b5..a3c9314e8d 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -140,7 +140,7 @@ android { buildConfigField "String", "BUILD_NUMBER", "\"${buildNumber}\"" resValue "string", "build_number", "\"${buildNumber}\"" - buildConfigField "im.vector.app.features.VectorFeatures.LoginVersion", "LOGIN_VERSION", "im.vector.app.features.VectorFeatures.LoginVersion.V1" + buildConfigField "im.vector.app.features.VectorFeatures.LoginVariant", "LOGIN_VARIANT", "im.vector.app.features.VectorFeatures.LoginVariant.LEGACY" buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 8d22fc599f..ca5d26aaeb 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -28,8 +28,8 @@ class DebugFeaturesStateFactory @Inject constructor( return FeaturesState(listOf( createEnumFeature( label = "Login version", - selection = debugFeatures.loginVersion(), - default = defaultFeatures.loginVersion() + selection = debugFeatures.loginVariant(), + default = defaultFeatures.loginVariant() ) )) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 0831609e4f..638509e76b 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -38,8 +38,8 @@ class DebugVectorFeatures( private val dataStore = context.dataStore - override fun loginVersion(): VectorFeatures.LoginVersion { - return readPreferences().getEnum() ?: vectorFeatures.loginVersion() + override fun loginVariant(): VectorFeatures.LoginVariant { + return readPreferences().getEnum() ?: vectorFeatures.loginVariant() } fun > hasEnumOverride(type: KClass) = readPreferences().containsEnum(type) 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 e106f7f75f..9453abe1db 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -20,11 +20,11 @@ import im.vector.app.BuildConfig interface VectorFeatures { - fun loginVersion(): LoginVersion + fun loginVariant(): LoginVariant - enum class LoginVersion { - V1, - V2 + enum class LoginVariant { + LEGACY, + FTUE_WIP } enum class NotificationSettingsVersion { @@ -34,5 +34,5 @@ interface VectorFeatures { } class DefaultVectorFeatures : VectorFeatures { - override fun loginVersion(): VectorFeatures.LoginVersion = BuildConfig.LOGIN_VERSION + override fun loginVariant(): VectorFeatures.LoginVariant = BuildConfig.LOGIN_VARIANT } 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 befc62d2ce..b18604a13f 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 @@ -110,18 +110,18 @@ class DefaultNavigator @Inject constructor( ) : Navigator { override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { - val intent = when (features.loginVersion()) { - VectorFeatures.LoginVersion.V1 -> LoginActivity.newIntent(context, loginConfig) - VectorFeatures.LoginVersion.V2 -> LoginActivity2.newIntent(context, loginConfig) + val intent = when (features.loginVariant()) { + VectorFeatures.LoginVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig) + VectorFeatures.LoginVariant.FTUE_WIP -> LoginActivity2.newIntent(context, loginConfig) } intent.addFlags(flags) context.startActivity(intent) } override fun loginSSORedirect(context: Context, data: Uri?) { - val intent = when (features.loginVersion()) { - VectorFeatures.LoginVersion.V1 -> LoginActivity.redirectIntent(context, data) - VectorFeatures.LoginVersion.V2 -> LoginActivity2.redirectIntent(context, data) + val intent = when (features.loginVariant()) { + VectorFeatures.LoginVariant.LEGACY -> LoginActivity.redirectIntent(context, data) + VectorFeatures.LoginVariant.FTUE_WIP -> LoginActivity2.redirectIntent(context, data) } context.startActivity(intent) } From 74594d8fc3095f554f50a97c3339711cba793936 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Dec 2021 12:06:29 +0000 Subject: [PATCH 5/5] porting the LoginActivty2 to a dynamic FTUE activity - supports switching between a copied legacy flow (DefaultFTUE) and the WIP variant - this will allow us to make iterative changes to the default ftue flow without affecting the legacy flow/forks --- vector/src/main/AndroidManifest.xml | 2 +- .../app/core/platform/VectorBaseActivity.kt | 2 +- .../im/vector/app/features/VectorFeatures.kt | 1 + .../app/features/ftue/DefaultFTUEVariant.kt | 367 ++++++++++++++++++ .../vector/app/features/ftue/FTUEActivity.kt | 85 ++++ .../app/features/ftue/FTUEVariantFactory.kt | 43 ++ .../FTUEWipVariant.kt} | 184 ++++----- .../login2/created/AccountCreatedFragment.kt | 4 +- .../features/navigation/DefaultNavigator.kt | 8 +- 9 files changed, 589 insertions(+), 107 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt rename vector/src/main/java/im/vector/app/features/{login2/LoginActivity2.kt => ftue/FTUEWipVariant.kt} (70%) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6d3c6cdc51..869e81b042 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -137,7 +137,7 @@ android:windowSoftInputMode="adjustResize" /> diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 181bd8c6be..4c92d70dce 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -104,7 +104,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), Maver protected val viewModelProvider get() = ViewModelProvider(this, viewModelFactory) - protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { viewEvents .stream() .onEach { 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 9453abe1db..58594be293 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -24,6 +24,7 @@ interface VectorFeatures { enum class LoginVariant { LEGACY, + FTUE, FTUE_WIP } diff --git a/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt new file mode 100644 index 0000000000..98b1f98df0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/DefaultFTUEVariant.kt @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2021 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.ftue + +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentTransaction +import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R +import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.extensions.addFragmentToBackstack +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.home.HomeActivity +import im.vector.app.features.login.LoginAction +import im.vector.app.features.login.LoginCaptchaFragment +import im.vector.app.features.login.LoginCaptchaFragmentArgument +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.login.LoginFragment +import im.vector.app.features.login.LoginGenericTextInputFormFragment +import im.vector.app.features.login.LoginGenericTextInputFormFragmentArgument +import im.vector.app.features.login.LoginMode +import im.vector.app.features.login.LoginResetPasswordFragment +import im.vector.app.features.login.LoginResetPasswordMailConfirmationFragment +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.LoginSplashFragment +import im.vector.app.features.login.LoginViewEvents +import im.vector.app.features.login.LoginViewModel +import im.vector.app.features.login.LoginViewState +import im.vector.app.features.login.LoginWaitForEmailFragment +import im.vector.app.features.login.LoginWaitForEmailFragmentArgument +import im.vector.app.features.login.LoginWebFragment +import im.vector.app.features.login.ServerType +import im.vector.app.features.login.SignMode +import im.vector.app.features.login.TextInputFormFragmentMode +import im.vector.app.features.login.isSupported +import im.vector.app.features.login.terms.LoginTermsFragment +import im.vector.app.features.login.terms.LoginTermsFragmentArgument +import im.vector.app.features.login.terms.toLocalizedLoginTerms +import org.matrix.android.sdk.api.auth.registration.FlowResult +import org.matrix.android.sdk.api.auth.registration.Stage +import org.matrix.android.sdk.api.extensions.tryOrNull + +private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" +private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" + +class DefaultFTUEVariant( + private val views: ActivityLoginBinding, + private val loginViewModel: LoginViewModel, + private val activity: VectorBaseActivity, + private val supportFragmentManager: FragmentManager +) : FTUEVariant { + + private val enterAnim = R.anim.enter_fade_in + private val exitAnim = R.anim.exit_fade_out + + private val popEnterAnim = R.anim.no_anim + private val popExitAnim = R.anim.exit_fade_out + + private val topFragment: Fragment? + get() = supportFragmentManager.findFragmentById(views.loginFragmentContainer.id) + + private val commonOption: (FragmentTransaction) -> Unit = { ft -> + // Find the loginLogo on the current Fragment, this should not return null + (topFragment?.view as? ViewGroup) + // Find findViewById does not work, I do not know why + // findViewById(R.id.loginLogo) + ?.children + ?.firstOrNull { it.id == R.id.loginLogo } + ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) + } + + override fun initUiAndData(isFirstCreation: Boolean) { + if (isFirstCreation) { + addFirstFragment() + } + + with(activity) { + loginViewModel.onEach { + updateWithState(it) + } + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } + } + + // Get config extra + val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + if (isFirstCreation) { + loginViewModel.handle(LoginAction.InitWith(loginConfig)) + } + } + + override fun setIsLoading(isLoading: Boolean) { + // do nothing + } + + private fun addFirstFragment() { + activity.addFragment(views.loginFragmentContainer, LoginSplashFragment::class.java) + } + + private fun handleLoginViewEvents(loginViewEvents: LoginViewEvents) { + when (loginViewEvents) { + 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 + onRegistrationStageNotSupported() + } else { + if (loginViewEvents.isRegistrationStarted) { + // Go on with registration flow + handleRegistrationNavigation(loginViewEvents.flowResult) + } else { + // First ask for login and password + // I add a tag to indicate that this fragment is a registration stage. + // This way it will be automatically popped in when starting the next registration stage + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption + ) + } + } + } + is LoginViewEvents.OutdatedHomeserver -> { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.login_error_outdated_homeserver_title) + .setMessage(R.string.login_error_outdated_homeserver_warning_content) + .setPositiveButton(R.string.ok, null) + .show() + Unit + } + is LoginViewEvents.OpenServerSelection -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginServerSelectionFragment::class.java, + option = { ft -> + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // Disable transition of text + // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // No transition here now actually + // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // 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 -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginSignUpSignInSelectionFragment::class.java, + option = commonOption) + is LoginViewEvents.OnWebLoginError -> onWebLoginError(loginViewEvents) + is LoginViewEvents.OnForgetPasswordClicked -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginResetPasswordFragment::class.java, + option = commonOption) + is LoginViewEvents.OnResetPasswordSendThreePidDone -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginResetPasswordMailConfirmationFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccess -> { + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginResetPasswordSuccessFragment::class.java, + option = commonOption) + } + is LoginViewEvents.OnResetPasswordMailConfirmationSuccessDone -> { + // Go back to the login fragment + supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) + } + is LoginViewEvents.OnSendEmailSuccess -> { + // Pop the enter email Fragment + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginWaitForEmailFragment::class.java, + LoginWaitForEmailFragmentArgument(loginViewEvents.email), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents.OnSendMsisdnSuccess -> { + // Pop the enter Msisdn Fragment + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, loginViewEvents.msisdn), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + } + is LoginViewEvents.Failure, + is LoginViewEvents.Loading -> + // This is handled by the Fragments + Unit + }.exhaustive + } + + private fun updateWithState(loginViewState: LoginViewState) { + if (loginViewState.isUserLogged()) { + val intent = HomeActivity.newIntent( + activity, + accountCreation = loginViewState.signMode == SignMode.SignUp + ) + activity.startActivity(intent) + activity.finish() + return + } + + // Loading + views.loginLoading.isVisible = loginViewState.isLoading() + } + + private fun onWebLoginError(onWebLoginError: LoginViewEvents.OnWebLoginError) { + // Pop the backstack + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + // And inform the user + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.dialog_title_error) + .setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) + .setPositiveButton(R.string.ok, null) + .show() + } + + private fun onServerSelectionDone(loginViewEvents: LoginViewEvents.OnServerSelectionDone) { + when (loginViewEvents.serverType) { + ServerType.MatrixOrg -> Unit // In this case, we wait for the login flow + ServerType.EMS, + ServerType.Other -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginServerUrlFormFragment::class.java, + option = commonOption) + 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 -> { + // This is managed by the LoginViewEvents + } + SignMode.SignIn -> { + // It depends on the LoginMode + when (state.loginMode) { + LoginMode.Unknown, + is LoginMode.Sso -> error("Developer error") + is LoginMode.SsoAndPassword, + LoginMode.Password -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + LoginMode.Unsupported -> onLoginModeNotSupported(state.loginModeSupportedTypes) + }.exhaustive + } + SignMode.SignInWithMatrixId -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginFragment::class.java, + tag = FRAGMENT_LOGIN_TAG, + option = commonOption) + }.exhaustive + } + + /** + * Handle the SSO redirection here + */ + override fun onNewIntent(intent: Intent?) { + intent?.data + ?.let { tryOrNull { it.getQueryParameter("loginToken") } } + ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } + } + + private fun onRegistrationStageNotSupported() { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_registration_not_supported)) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun onLoginModeNotSupported(supportedTypes: List) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_name) + .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setPositiveButton(R.string.yes) { _, _ -> + activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginWebFragment::class.java, + option = commonOption) + } + .setNegativeButton(R.string.no, null) + .show() + } + + private fun handleRegistrationNavigation(flowResult: FlowResult) { + // Complete all mandatory stages first + val mandatoryStage = flowResult.missingStages.firstOrNull { it.mandatory } + + if (mandatoryStage != null) { + doStage(mandatoryStage) + } else { + // Consider optional stages + val optionalStage = flowResult.missingStages.firstOrNull { !it.mandatory && it !is Stage.Dummy } + if (optionalStage == null) { + // Should not happen... + } else { + doStage(optionalStage) + } + } + } + + private fun doStage(stage: Stage) { + // Ensure there is no fragment for registration stage in the backstack + supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) + + when (stage) { + is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginCaptchaFragment::class.java, + LoginCaptchaFragmentArgument(stage.publicKey), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginGenericTextInputFormFragment::class.java, + LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer, + LoginTermsFragment::class.java, + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), + tag = FRAGMENT_REGISTRATION_STAGE_TAG, + option = commonOption) + else -> Unit // Should not happen + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt new file mode 100644 index 0000000000..805e39c48d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEActivity.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021 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.ftue + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.google.android.material.appbar.MaterialToolbar +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.lazyViewModel +import im.vector.app.core.platform.ToolbarConfigurable +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.core.platform.lifecycleAwareLazy +import im.vector.app.databinding.ActivityLoginBinding +import im.vector.app.features.login.LoginConfig +import im.vector.app.features.pin.UnlockedActivity +import javax.inject.Inject + +@AndroidEntryPoint +class FTUEActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { + + private val ftueVariant by lifecycleAwareLazy { + ftueVariantFactory.create(this, loginViewModel = lazyViewModel(), loginViewModel2 = lazyViewModel()) + } + + @Inject lateinit var ftueVariantFactory: FTUEVariantFactory + + override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun configure(toolbar: MaterialToolbar) { + configureToolbar(toolbar) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + ftueVariant.onNewIntent(intent) + } + + override fun initUiAndData() { + ftueVariant.initUiAndData(isFirstCreation()) + } + + // Hack for AccountCreatedFragment + fun setIsLoading(isLoading: Boolean) { + ftueVariant.setIsLoading(isLoading) + } + + companion object { + const val EXTRA_CONFIG = "EXTRA_CONFIG" + + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { + return Intent(context, FTUEActivity::class.java).apply { + putExtra(EXTRA_CONFIG, loginConfig) + } + } + + fun redirectIntent(context: Context, data: Uri?): Intent { + return Intent(context, FTUEActivity::class.java).apply { + setData(data) + } + } + } +} + +interface FTUEVariant { + fun onNewIntent(intent: Intent?) + fun initUiAndData(isFirstCreation: Boolean) + fun setIsLoading(isLoading: Boolean) +} diff --git a/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt new file mode 100644 index 0000000000..7efd6023fe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEVariantFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 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.ftue + +import im.vector.app.features.VectorFeatures +import im.vector.app.features.login.LoginViewModel +import im.vector.app.features.login2.LoginViewModel2 +import javax.inject.Inject + +class FTUEVariantFactory @Inject constructor( + private val vectorFeatures: VectorFeatures, +) { + + fun create(activity: FTUEActivity, loginViewModel: Lazy, loginViewModel2: Lazy) = when (vectorFeatures.loginVariant()) { + VectorFeatures.LoginVariant.LEGACY -> error("Legacy is not supported by the FTUE") + VectorFeatures.LoginVariant.FTUE -> DefaultFTUEVariant( + views = activity.getBinding(), + loginViewModel = loginViewModel.value, + activity = activity, + supportFragmentManager = activity.supportFragmentManager + ) + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEWipVariant( + views = activity.getBinding(), + loginViewModel = loginViewModel2.value, + activity = activity, + supportFragmentManager = activity.supportFragmentManager + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt b/vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt similarity index 70% rename from vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt rename to vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt index ce9d9f762e..c1fc49db00 100644 --- a/vector/src/main/java/im/vector/app/features/login2/LoginActivity2.kt +++ b/vector/src/main/java/im/vector/app/features/ftue/FTUEWipVariant.kt @@ -1,11 +1,11 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2021 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 + * 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, @@ -14,11 +14,9 @@ * limitations under the License. */ -package im.vector.app.features.login2 +package im.vector.app.features.ftue -import android.content.Context import android.content.Intent -import android.net.Uri import android.view.View import android.view.ViewGroup import androidx.core.view.ViewCompat @@ -27,17 +25,13 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction -import com.airbnb.mvrx.viewModel -import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.POP_BACK_STACK_EXCLUSIVE import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.resetBackstack -import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityLoginBinding import im.vector.app.features.home.HomeActivity @@ -49,20 +43,41 @@ import im.vector.app.features.login.TextInputFormFragmentMode import im.vector.app.features.login.isSupported import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.toLocalizedLoginTerms +import im.vector.app.features.login2.LoginAction2 +import im.vector.app.features.login2.LoginCaptchaFragment2 +import im.vector.app.features.login2.LoginFragmentSigninPassword2 +import im.vector.app.features.login2.LoginFragmentSigninUsername2 +import im.vector.app.features.login2.LoginFragmentSignupPassword2 +import im.vector.app.features.login2.LoginFragmentSignupUsername2 +import im.vector.app.features.login2.LoginFragmentToAny2 +import im.vector.app.features.login2.LoginGenericTextInputFormFragment2 +import im.vector.app.features.login2.LoginResetPasswordFragment2 +import im.vector.app.features.login2.LoginResetPasswordMailConfirmationFragment2 +import im.vector.app.features.login2.LoginResetPasswordSuccessFragment2 +import im.vector.app.features.login2.LoginServerSelectionFragment2 +import im.vector.app.features.login2.LoginServerUrlFormFragment2 +import im.vector.app.features.login2.LoginSplashSignUpSignInSelectionFragment2 +import im.vector.app.features.login2.LoginSsoOnlyFragment2 +import im.vector.app.features.login2.LoginViewEvents2 +import im.vector.app.features.login2.LoginViewModel2 +import im.vector.app.features.login2.LoginViewState2 +import im.vector.app.features.login2.LoginWaitForEmailFragment2 +import im.vector.app.features.login2.LoginWebFragment2 import im.vector.app.features.login2.created.AccountCreatedFragment import im.vector.app.features.login2.terms.LoginTermsFragment2 -import im.vector.app.features.pin.UnlockedActivity import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.extensions.tryOrNull -/** - * The LoginActivity manages the fragment navigation and also display the loading View - */ -@AndroidEntryPoint -open class LoginActivity2 : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { +private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" +private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" - private val loginViewModel: LoginViewModel2 by viewModel() +class FTUEWipVariant( + private val views: ActivityLoginBinding, + private val loginViewModel: LoginViewModel2, + private val activity: VectorBaseActivity, + private val supportFragmentManager: FragmentManager +) : FTUEVariant { private val enterAnim = R.anim.enter_fade_in private val exitAnim = R.anim.exit_fade_out @@ -76,39 +91,36 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private val commonOption: (FragmentTransaction) -> Unit = { ft -> // Find the loginLogo on the current Fragment, this should not return null (topFragment?.view as? ViewGroup) - // Find findViewById does not work, I do not know why - // findViewById(R.id.loginLogo) + // Find activity.findViewById does not work, I do not know why + // activity.findViewById(views.loginLogo) ?.children ?.firstOrNull { it.id == R.id.loginLogo } ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) } - final override fun getBinding() = ActivityLoginBinding.inflate(layoutInflater) - - override fun getCoordinatorLayout() = views.coordinatorLayout - - override fun initUiAndData() { - if (isFirstCreation()) { + override fun initUiAndData(isFirstCreation: Boolean) { + if (isFirstCreation) { addFirstFragment() } - loginViewModel.onEach { - updateWithState(it) + with(activity) { + loginViewModel.onEach { + updateWithState(it) + } + loginViewModel.observeViewEvents { handleLoginViewEvents(it) } } - loginViewModel.observeViewEvents { handleLoginViewEvents(it) } - // Get config extra - val loginConfig = intent.getParcelableExtra(EXTRA_CONFIG) - if (isFirstCreation()) { + val loginConfig = activity.intent.getParcelableExtra(FTUEActivity.EXTRA_CONFIG) + if (isFirstCreation) { // TODO Check this loginViewModel.handle(LoginAction2.InitWith(loginConfig)) } } - protected open fun addFirstFragment() { - addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java) + private fun addFirstFragment() { + activity.addFragment(views.loginFragmentContainer, LoginSplashSignUpSignInSelectionFragment2::class.java) } private fun handleLoginViewEvents(event: LoginViewEvents2) { @@ -127,7 +139,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC // First ask for login and password // I add a tag to indicate that this fragment is a registration stage. // This way it will be automatically popped in when starting the next registration stage - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragment2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption @@ -138,7 +150,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } } is LoginViewEvents2.OutdatedHomeserver -> { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.login_error_outdated_homeserver_title) .setMessage(R.string.login_error_outdated_homeserver_warning_content) .setPositiveButton(R.string.ok, null) @@ -146,54 +158,54 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC Unit } is LoginViewEvents2.OpenServerSelection -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginServerSelectionFragment2::class.java, option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // Disable transition of text - // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // No transition here now actually - // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) is LoginViewEvents2.OpenHomeServerUrlFormScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginServerUrlFormFragment2::class.java, option = commonOption) } is LoginViewEvents2.OpenSignInEnterIdentifierScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSigninUsername2::class.java, option = { ft -> - findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + activity.findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // Disable transition of text - // findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // No transition here now actually - // findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } + // activity.findViewById(views.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering // ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) }) } is LoginViewEvents2.OpenSsoOnlyScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginSsoOnlyFragment2::class.java, option = commonOption) } is LoginViewEvents2.OnWebLoginError -> onWebLoginError(event) is LoginViewEvents2.OpenResetPasswordScreen -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordFragment2::class.java, option = commonOption) is LoginViewEvents2.OnResetPasswordSendThreePidDone -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordMailConfirmationFragment2::class.java, option = commonOption) } is LoginViewEvents2.OnResetPasswordMailConfirmationSuccess -> { supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginResetPasswordSuccessFragment2::class.java, option = commonOption) } @@ -202,37 +214,37 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC supportFragmentManager.popBackStack(FRAGMENT_LOGIN_TAG, POP_BACK_STACK_EXCLUSIVE) } is LoginViewEvents2.OnSendEmailSuccess -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWaitForEmailFragment2::class.java, LoginWaitForEmailFragmentArgument(event.email), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) is LoginViewEvents2.OpenSigninPasswordScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSigninPassword2::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OpenSignupPasswordScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSignupPassword2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } is LoginViewEvents2.OpenSignUpChooseUsernameScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentSignupUsername2::class.java, tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) } is LoginViewEvents2.OpenSignInWithAnythingScreen -> { - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginFragmentToAny2::class.java, tag = FRAGMENT_LOGIN_TAG, option = commonOption) } is LoginViewEvents2.OnSendMsisdnSuccess -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.ConfirmMsisdn, true, event.msisdn), tag = FRAGMENT_REGISTRATION_STAGE_TAG, @@ -250,14 +262,14 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private fun handleCancelRegistration() { // Cleanup the back stack - resetBackstack() + activity.resetBackstack() } private fun handleOnSessionCreated(event: LoginViewEvents2.OnSessionCreated) { if (event.newAccount) { // Propose to set avatar and display name // Back on this Fragment will finish the Activity - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, AccountCreatedFragment::class.java, option = commonOption) } else { @@ -267,11 +279,11 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC private fun terminate(newAccount: Boolean) { val intent = HomeActivity.newIntent( - this, + activity, accountCreation = newAccount ) - startActivity(intent) - finish() + activity.startActivity(intent) + activity.finish() } private fun updateWithState(LoginViewState2: LoginViewState2) { @@ -280,7 +292,7 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } // Hack for AccountCreatedFragment - fun setIsLoading(isLoading: Boolean) { + override fun setIsLoading(isLoading: Boolean) { views.loginLoading.isVisible = isLoading } @@ -289,9 +301,9 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) // And inform the user - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.dialog_title_error) - .setMessage(getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) + .setMessage(activity.getString(R.string.login_sso_error_message, onWebLoginError.description, onWebLoginError.errorCode)) .setPositiveButton(R.string.ok, null) .show() } @@ -300,19 +312,17 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC * Handle the SSO redirection here */ override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) - intent?.data ?.let { tryOrNull { it.getQueryParameter("loginToken") } } ?.let { loginViewModel.handle(LoginAction2.LoginWithToken(it)) } } private fun onRegistrationStageNotSupported() { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_name) - .setMessage(getString(R.string.login_registration_not_supported)) + .setMessage(activity.getString(R.string.login_registration_not_supported)) .setPositiveButton(R.string.yes) { _, _ -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWebFragment2::class.java, option = commonOption) } @@ -321,11 +331,11 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC } private fun onLoginModeNotSupported(supportedTypes: List) { - MaterialAlertDialogBuilder(this) + MaterialAlertDialogBuilder(activity) .setTitle(R.string.app_name) - .setMessage(getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) + .setMessage(activity.getString(R.string.login_mode_not_supported, supportedTypes.joinToString { "'$it'" })) .setPositiveButton(R.string.yes) { _, _ -> - addFragmentToBackstack(views.loginFragmentContainer, + activity.addFragmentToBackstack(views.loginFragmentContainer, LoginWebFragment2::class.java, option = commonOption) } @@ -355,53 +365,27 @@ open class LoginActivity2 : VectorBaseActivity(), ToolbarC supportFragmentManager.popBackStack(FRAGMENT_REGISTRATION_STAGE_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE) when (stage) { - is Stage.ReCaptcha -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.ReCaptcha -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginCaptchaFragment2::class.java, LoginCaptchaFragmentArgument(stage.publicKey), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Email -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.Email -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetEmail, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Msisdn -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.Msisdn -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginGenericTextInputFormFragment2::class.java, LoginGenericTextInputFormFragmentArgument(TextInputFormFragmentMode.SetMsisdn, stage.mandatory), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) - is Stage.Terms -> addFragmentToBackstack(views.loginFragmentContainer, + is Stage.Terms -> activity.addFragmentToBackstack(views.loginFragmentContainer, LoginTermsFragment2::class.java, - LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(getString(R.string.resources_language))), + LoginTermsFragmentArgument(stage.policies.toLocalizedLoginTerms(activity.getString(R.string.resources_language))), tag = FRAGMENT_REGISTRATION_STAGE_TAG, option = commonOption) else -> Unit // Should not happen } } - - override fun configure(toolbar: MaterialToolbar) { - configureToolbar(toolbar) - } - - companion object { - private const val FRAGMENT_REGISTRATION_STAGE_TAG = "FRAGMENT_REGISTRATION_STAGE_TAG" - private const val FRAGMENT_LOGIN_TAG = "FRAGMENT_LOGIN_TAG" - - private const val EXTRA_CONFIG = "EXTRA_CONFIG" - - // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string - const val VECTOR_REDIRECT_URL = "element://connect" - - fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { - return Intent(context, LoginActivity2::class.java).apply { - putExtra(EXTRA_CONFIG, loginConfig) - } - } - - fun redirectIntent(context: Context, data: Uri?): Intent { - return Intent(context, LoginActivity2::class.java).apply { - setData(data) - } - } - } } diff --git a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt index 94784b0605..c1f45c6713 100644 --- a/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login2/created/AccountCreatedFragment.kt @@ -34,11 +34,11 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.FragmentLoginAccountCreatedBinding import im.vector.app.features.displayname.getBestName +import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.login2.AbstractLoginFragment2 import im.vector.app.features.login2.LoginAction2 -import im.vector.app.features.login2.LoginActivity2 import im.vector.app.features.login2.LoginViewState2 import org.matrix.android.sdk.api.util.MatrixItem import java.util.UUID @@ -130,7 +130,7 @@ class AccountCreatedFragment @Inject constructor( private fun invalidateState(state: AccountCreatedViewState) { // Ugly hack... - (activity as? LoginActivity2)?.setIsLoading(state.isLoading) + (activity as? FTUEActivity)?.setIsLoading(state.isLoading) views.loginAccountCreatedSubtitle.text = getString(R.string.login_account_created_subtitle, state.userId) 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 b18604a13f..0c335d7ddc 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 @@ -50,6 +50,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity import im.vector.app.features.devtools.RoomDevToolActivity +import im.vector.app.features.ftue.FTUEActivity import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.home.room.detail.search.SearchActivity @@ -58,7 +59,6 @@ import im.vector.app.features.home.room.filtered.FilteredRoomsActivity import im.vector.app.features.invite.InviteUsersToRoomActivity import im.vector.app.features.login.LoginActivity import im.vector.app.features.login.LoginConfig -import im.vector.app.features.login2.LoginActivity2 import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.BigImageViewerActivity @@ -112,7 +112,8 @@ class DefaultNavigator @Inject constructor( override fun openLogin(context: Context, loginConfig: LoginConfig?, flags: Int) { val intent = when (features.loginVariant()) { VectorFeatures.LoginVariant.LEGACY -> LoginActivity.newIntent(context, loginConfig) - VectorFeatures.LoginVariant.FTUE_WIP -> LoginActivity2.newIntent(context, loginConfig) + VectorFeatures.LoginVariant.FTUE, + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.newIntent(context, loginConfig) } intent.addFlags(flags) context.startActivity(intent) @@ -121,7 +122,8 @@ class DefaultNavigator @Inject constructor( override fun loginSSORedirect(context: Context, data: Uri?) { val intent = when (features.loginVariant()) { VectorFeatures.LoginVariant.LEGACY -> LoginActivity.redirectIntent(context, data) - VectorFeatures.LoginVariant.FTUE_WIP -> LoginActivity2.redirectIntent(context, data) + VectorFeatures.LoginVariant.FTUE, + VectorFeatures.LoginVariant.FTUE_WIP -> FTUEActivity.redirectIntent(context, data) } context.startActivity(intent) }