From 690fda180c339b4710b50d800cc74c3ae441b1a6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 11 May 2022 18:29:44 +0100 Subject: [PATCH] providing dedicated reset action for resetting invalid deeplink homeserver - also fixes the usecase screen becoming stuck with an invalid homeserver deeplink --- .../features/onboarding/OnboardingAction.kt | 5 +- .../onboarding/OnboardingViewModel.kt | 15 ++-- .../FtueAuthSplashCarouselFragment.kt | 75 ++++++++++++++----- .../ftueauth/FtueAuthSplashFragment.kt | 7 +- .../ftueauth/FtueAuthUseCaseFragment.kt | 32 +++++++- 5 files changed, 101 insertions(+), 33 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index bef624ddc4..97a631dd1f 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -25,8 +25,9 @@ import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.network.ssl.Fingerprint sealed interface OnboardingAction : VectorViewModelAction { - data class OnGetStarted(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction - data class OnIAlreadyHaveAnAccount(val resetLoginConfig: Boolean, val onboardingFlow: OnboardingFlow) : OnboardingAction + object ResetDeeplinkConfig : OnboardingAction + data class OnGetStarted(val onboardingFlow: OnboardingFlow) : OnboardingAction + data class OnIAlreadyHaveAnAccount(val onboardingFlow: OnboardingFlow) : OnboardingAction data class UpdateServerType(val serverType: ServerType) : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 0bd61758bc..a7de3c157a 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -30,6 +30,7 @@ import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.vectorStore import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorOverrides @@ -110,7 +111,8 @@ class OnboardingViewModel @AssistedInject constructor( private var currentHomeServerConnectionConfig: HomeServerConnectionConfig? = null private val matrixOrgUrl = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() - private val defaultHomeserverUrl = matrixOrgUrl + private val defaultHomeserverUrl: String + get() = loginConfig?.homeServerUrl?.ensureProtocol() ?: matrixOrgUrl private val registrationWizard: RegistrationWizard get() = authenticationService.getRegistrationWizard() @@ -132,8 +134,8 @@ class OnboardingViewModel @AssistedInject constructor( override fun handle(action: OnboardingAction) { when (action) { - is OnboardingAction.OnGetStarted -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow) - is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.resetLoginConfig, action.onboardingFlow) + is OnboardingAction.OnGetStarted -> handleSplashAction(action.onboardingFlow) + is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.onboardingFlow) is OnboardingAction.UpdateUseCase -> handleUpdateUseCase(action) OnboardingAction.ResetUseCase -> resetUseCase() is OnboardingAction.UpdateServerType -> handleUpdateServerType(action) @@ -157,6 +159,7 @@ class OnboardingViewModel @AssistedInject constructor( OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) OnboardingAction.StopEmailValidationCheck -> cancelWaitForEmailValidation() + OnboardingAction.ResetDeeplinkConfig -> loginConfig = null } } @@ -173,10 +176,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleSplashAction(resetConfig: Boolean, onboardingFlow: OnboardingFlow) { - if (resetConfig) { - loginConfig = null - } + private fun handleSplashAction(onboardingFlow: OnboardingFlow) { setState { copy(onboardingFlow = onboardingFlow) } return when (val config = loginConfig.toHomeserverConfig()) { @@ -422,7 +422,6 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleInitWith(action: OnboardingAction.InitWith) { loginConfig = action.loginConfig - // If there is a pending email validation continue on this step try { if (registrationWizard.isRegistrationStarted) { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt index 30416bde9e..8306589e89 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashCarouselFragment.kt @@ -17,6 +17,9 @@ package im.vector.app.features.onboarding.ftueauth import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -41,8 +44,7 @@ import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.failure.Failure -import java.net.UnknownHostException +import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import javax.inject.Inject private const val CAROUSEL_ROTATION_DELAY_MS = 5000L @@ -128,11 +130,11 @@ class FtueAuthSplashCarouselFragment @Inject constructor( private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp - viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow)) + viewModel.handle(OnboardingAction.OnGetStarted(onboardingFlow = getStartedFlow)) } private fun alreadyHaveAnAccount() { - viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(resetLoginConfig = false, onboardingFlow = OnboardingFlow.SignIn)) + viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn)) } override fun resetViewModel() { @@ -140,21 +142,56 @@ class FtueAuthSplashCarouselFragment @Inject constructor( } override fun onError(throwable: Throwable) { - if (throwable is Failure.NetworkConnection && - throwable.ioException is UnknownHostException) { - // Invalid homeserver from URL config - val url = viewModel.getInitialHomeServerUrl().orEmpty() - MaterialAlertDialogBuilder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url)) - .setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> - val flow = withState(viewModel) { it.onboardingFlow } ?: OnboardingFlow.SignInSignUp - viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = true, flow)) - } - .setNegativeButton(R.string.action_cancel, null) - .show() - } else { - super.onError(throwable) + when { + requireContext().inferNoConnectivity() -> super.onError(throwable) + throwable.isHomeserverUnavailable() -> { + val url = viewModel.getInitialHomeServerUrl().orEmpty() + homeserverUnavailableDialog(url) { onContinueFlowWithLoginConfigReset() } + } + else -> super.onError(throwable) } } + + private fun onContinueFlowWithLoginConfigReset() { + viewModel.handle(OnboardingAction.ResetDeeplinkConfig) + when (val flow = withState(viewModel) { it.onboardingFlow } ?: OnboardingFlow.SignInSignUp) { + OnboardingFlow.SignIn -> if (vectorFeatures.isOnboardingCombinedLoginEnabled()) { + viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(flow)) + } else { + viewModel.handle(OnboardingAction.OnGetStarted(flow)) + } + else -> viewModel.handle(OnboardingAction.OnGetStarted(flow)) + } + } + + private fun homeserverUnavailableDialog(url: String, action: () -> Unit) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url)) + .setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> action() } + .setNegativeButton(R.string.action_cancel, null) + .show() + } +} + +fun Context.inferNoConnectivity(): Boolean { + var networkAvailable = false + + val connectivityManager: ConnectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) + + when { + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> { + networkAvailable = true + } + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> { + networkAvailable = true + } + networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true -> { + networkAvailable = true + } + } + + return !networkAvailable } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt index 2fa3b52706..c961986fd2 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthSplashFragment.kt @@ -75,11 +75,11 @@ class FtueAuthSplashFragment @Inject constructor( private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp - viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = false, onboardingFlow = getStartedFlow)) + viewModel.handle(OnboardingAction.OnGetStarted(onboardingFlow = getStartedFlow)) } private fun alreadyHaveAnAccount() { - viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(resetLoginConfig = false, onboardingFlow = OnboardingFlow.SignIn)) + viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn)) } override fun resetViewModel() { @@ -96,7 +96,8 @@ class FtueAuthSplashFragment @Inject constructor( .setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url)) .setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> val flow = withState(viewModel) { it.onboardingFlow } ?: OnboardingFlow.SignInSignUp - viewModel.handle(OnboardingAction.OnGetStarted(resetLoginConfig = true, flow)) + viewModel.handle(OnboardingAction.ResetDeeplinkConfig) + viewModel.handle(OnboardingAction.OnGetStarted(flow)) } .setNegativeButton(R.string.action_cancel, null) .show() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt index 5325b25e93..41675e20ca 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthUseCaseFragment.kt @@ -28,6 +28,8 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat +import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.extensions.getResTintedDrawable import im.vector.app.core.extensions.getTintedDrawable @@ -38,13 +40,14 @@ import im.vector.app.features.login.ServerType import im.vector.app.features.onboarding.FtueUseCase import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.themes.ThemeProvider +import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import javax.inject.Inject private const val DARK_MODE_ICON_BACKGROUND_ALPHA = 0.30f private const val LIGHT_MODE_ICON_BACKGROUND_ALPHA = 0.15f class FtueAuthUseCaseFragment @Inject constructor( - private val themeProvider: ThemeProvider + private val themeProvider: ThemeProvider, ) : AbstractFtueAuthFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAuthUseCaseBinding { @@ -111,4 +114,31 @@ class FtueAuthUseCaseFragment @Inject constructor( val whiteLayer = context.getTintedDrawable(R.drawable.bg_feature_icon, Color.WHITE) return LayerDrawable(arrayOf(whiteLayer, iconBackground, ContextCompat.getDrawable(context, icon))) } + + override fun onError(throwable: Throwable) { + when { + requireContext().inferNoConnectivity() -> super.onError(throwable) + throwable.isHomeserverUnavailable() -> { + val url = viewModel.getInitialHomeServerUrl().orEmpty() + homeserverUnavailableDialog(url) { onContinueFlowWithLoginConfigReset() } + } + else -> super.onError(throwable) + } + } + + private fun onContinueFlowWithLoginConfigReset() { + viewModel.handle(OnboardingAction.ResetDeeplinkConfig) + withState(viewModel) { it.useCase }?.let { + viewModel.handle(OnboardingAction.UpdateUseCase(it)) + } + } + + private fun homeserverUnavailableDialog(url: String, action: () -> Unit) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(getString(R.string.login_error_homeserver_from_url_not_found, url)) + .setPositiveButton(R.string.login_error_homeserver_from_url_not_found_enter_manual) { _, _ -> action() } + .setNegativeButton(R.string.action_cancel, null) + .show() + } }