lifting unavailable homeserver rendering to the activity/ftuevariant

- the viewmodel is now responsible for inferring connectivity errors and providing a retry action
This commit is contained in:
Adam Brown 2022-05-12 11:49:46 +01:00
parent 100aa24021
commit ea7df9b673
8 changed files with 58 additions and 129 deletions

View File

@ -25,8 +25,12 @@ 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 onboardingFlow: OnboardingFlow) : OnboardingAction
data class OnIAlreadyHaveAnAccount(val onboardingFlow: OnboardingFlow) : OnboardingAction
sealed interface SplashAction: OnboardingAction {
val onboardingFlow: OnboardingFlow
data class OnGetStarted(override val onboardingFlow: OnboardingFlow) : SplashAction
data class OnIAlreadyHaveAnAccount(override val onboardingFlow: OnboardingFlow) : SplashAction
}
data class UpdateServerType(val serverType: ServerType) : OnboardingAction

View File

@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.auth.registration.FlowResult
sealed class OnboardingViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OnboardingViewEvents()
data class Failure(val throwable: Throwable) : OnboardingViewEvents()
data class DeeplinkAuthenticationFailure(val retryAction: OnboardingAction) : OnboardingViewEvents()
data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : OnboardingViewEvents()
object OutdatedHomeserver : OnboardingViewEvents()

View File

@ -27,6 +27,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.cancelCurrentOnSet
import im.vector.app.core.extensions.configureAndStart
import im.vector.app.core.extensions.inferNoConnectivity
import im.vector.app.core.extensions.vectorStore
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
@ -56,6 +57,7 @@ import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
import java.util.UUID
@ -133,8 +135,7 @@ class OnboardingViewModel @AssistedInject constructor(
override fun handle(action: OnboardingAction) {
when (action) {
is OnboardingAction.OnGetStarted -> handleSplashAction(action.onboardingFlow)
is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.onboardingFlow)
is OnboardingAction.SplashAction -> handleSplashAction(action)
is OnboardingAction.UpdateUseCase -> handleUpdateUseCase(action)
OnboardingAction.ResetUseCase -> resetUseCase()
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
@ -174,9 +175,9 @@ class OnboardingViewModel @AssistedInject constructor(
}
}
private fun handleSplashAction(onboardingFlow: OnboardingFlow) {
setState { copy(onboardingFlow = onboardingFlow) }
continueToPageAfterSplash(onboardingFlow)
private fun handleSplashAction(action: OnboardingAction.SplashAction) {
setState { copy(onboardingFlow = action.onboardingFlow) }
continueToPageAfterSplash(action.onboardingFlow)
}
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
@ -629,12 +630,28 @@ class OnboardingViewModel @AssistedInject constructor(
setState { copy(isLoading = true) }
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) },
onFailure = { _viewEvents.post(OnboardingViewEvents.Failure(it)) }
onFailure = { onAuthenticationStartError(it, trigger) }
)
setState { copy(isLoading = false) }
}
}
private fun onAuthenticationStartError(it: Throwable, trigger: OnboardingAction.HomeServerChange) {
when {
it.isHomeserverUnavailable() && applicationContext.inferNoConnectivity() -> _viewEvents.post(
OnboardingViewEvents.Failure(it)
)
it.isHomeserverUnavailable() && trigger is OnboardingAction.HomeServerChange.SelectHomeServer -> _viewEvents.post(
OnboardingViewEvents.DeeplinkAuthenticationFailure(retryAction = trigger.resetToDefaultUrl())
)
else -> _viewEvents.post(
OnboardingViewEvents.Failure(it)
)
}
}
private fun OnboardingAction.HomeServerChange.SelectHomeServer.resetToDefaultUrl() = copy(homeServerUrl = defaultHomeserverUrl)
private suspend fun onAuthenticationStartedSuccess(
trigger: OnboardingAction.HomeServerChange,
config: HomeServerConnectionConfig,

View File

@ -17,9 +17,6 @@
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
@ -29,8 +26,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator
import im.vector.app.BuildConfig
import im.vector.app.R
@ -44,7 +39,6 @@ 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.isHomeserverUnavailable
import javax.inject.Inject
private const val CAROUSEL_ROTATION_DELAY_MS = 5000L
@ -130,68 +124,14 @@ class FtueAuthSplashCarouselFragment @Inject constructor(
private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) {
val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp
viewModel.handle(OnboardingAction.OnGetStarted(onboardingFlow = getStartedFlow))
viewModel.handle(OnboardingAction.SplashAction.OnGetStarted(onboardingFlow = getStartedFlow))
}
private fun alreadyHaveAnAccount() {
viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn))
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn))
}
override fun resetViewModel() {
// Nothing to do
}
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)
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
}

View File

@ -22,8 +22,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.databinding.FragmentFtueAuthSplashBinding
@ -31,8 +29,6 @@ import im.vector.app.features.VectorFeatures
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.failure.Failure
import java.net.UnknownHostException
import javax.inject.Inject
/**
@ -75,34 +71,14 @@ class FtueAuthSplashFragment @Inject constructor(
private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) {
val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp
viewModel.handle(OnboardingAction.OnGetStarted(onboardingFlow = getStartedFlow))
viewModel.handle(OnboardingAction.SplashAction.OnGetStarted(onboardingFlow = getStartedFlow))
}
private fun alreadyHaveAnAccount() {
viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn))
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn))
}
override fun resetViewModel() {
// Nothing to do
}
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.ResetDeeplinkConfig)
viewModel.handle(OnboardingAction.OnGetStarted(flow))
}
.setNegativeButton(R.string.action_cancel, null)
.show()
} else {
super.onError(throwable)
}
}
}

View File

@ -28,8 +28,6 @@ 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
@ -40,7 +38,6 @@ 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
@ -107,38 +104,11 @@ class FtueAuthUseCaseFragment @Inject constructor(
private fun createIcon(@ColorRes tint: Int, icon: Int, isLightMode: Boolean): Drawable {
val context = requireContext()
val alpha = when (isLightMode) {
true -> LIGHT_MODE_ICON_BACKGROUND_ALPHA
true -> LIGHT_MODE_ICON_BACKGROUND_ALPHA
false -> DARK_MODE_ICON_BACKGROUND_ALPHA
}
val iconBackground = context.getResTintedDrawable(R.drawable.bg_feature_icon, tint, alpha = alpha)
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()
}
}

View File

@ -227,11 +227,28 @@ class FtueAuthVariant(
option = commonOption
)
}
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin()
is OnboardingViewEvents.DeeplinkAuthenticationFailure -> onDeeplinkedHomeserverUnavailable(viewEvents)
}
}
private fun onDeeplinkedHomeserverUnavailable(viewEvents: OnboardingViewEvents.DeeplinkAuthenticationFailure) {
showHomeserverUnavailableDialog(onboardingViewModel.getInitialHomeServerUrl().orEmpty()) {
onboardingViewModel.handle(OnboardingAction.ResetDeeplinkConfig)
onboardingViewModel.handle(viewEvents.retryAction)
}
}
private fun showHomeserverUnavailableDialog(url: String, action: () -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_title_error)
.setMessage(activity.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()
}
private fun onStartCombinedLogin() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java)
}

View File

@ -17,13 +17,17 @@
package im.vector.app.features.onboarding.ftueauth
import android.widget.Button
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.extensions.hasContentFlow
import im.vector.app.core.extensions.inferNoConnectivity
import im.vector.app.features.login.SignMode
import im.vector.app.features.onboarding.OnboardingAction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
return when (this) {