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
This commit is contained in:
Adam Brown 2021-12-08 12:06:29 +00:00
parent fd0e1e44c4
commit 74594d8fc3
9 changed files with 589 additions and 107 deletions

View File

@ -137,7 +137,7 @@
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".features.login2.LoginActivity2"
android:name=".features.ftue.FTUEActivity"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize" />

View File

@ -104,7 +104,7 @@ abstract class VectorBaseActivity<VB : ViewBinding> : AppCompatActivity(), Maver
protected val viewModelProvider
get() = ViewModelProvider(this, viewModelFactory)
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {

View File

@ -24,6 +24,7 @@ interface VectorFeatures {
enum class LoginVariant {
LEGACY,
FTUE,
FTUE_WIP
}

View File

@ -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<ActivityLoginBinding>,
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<View?>(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<LoginConfig?>(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<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(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<String>) {
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
}
}
}

View File

@ -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<ActivityLoginBinding>(), 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)
}

View File

@ -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<LoginViewModel>, loginViewModel2: Lazy<LoginViewModel2>) = 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
)
}
}

View File

@ -1,5 +1,5 @@
/*
* 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.
@ -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<ActivityLoginBinding>(), 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<ActivityLoginBinding>,
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<ActivityLoginBinding>(), 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<View?>(R.id.loginLogo)
// Find activity.findViewById does not work, I do not know why
// activity.findViewById<View?>(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()
}
with(activity) {
loginViewModel.onEach {
updateWithState(it)
}
loginViewModel.observeViewEvents { handleLoginViewEvents(it) }
}
// Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
if (isFirstCreation()) {
val loginConfig = activity.intent.getParcelableExtra<LoginConfig?>(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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), ToolbarC
Unit
}
is LoginViewEvents2.OpenServerSelection ->
addFragmentToBackstack(views.loginFragmentContainer,
activity.addFragmentToBackstack(views.loginFragmentContainer,
LoginServerSelectionFragment2::class.java,
option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// activity.findViewById<View?>(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<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
activity.findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// Disable transition of text
// findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// activity.findViewById<View?>(views.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// No transition here now actually
// findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// activity.findViewById<View?>(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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), 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<ActivityLoginBinding>(), ToolbarC
}
private fun onLoginModeNotSupported(supportedTypes: List<String>) {
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<ActivityLoginBinding>(), 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)
}
}
}
}

View File

@ -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)

View File

@ -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)
}