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 import org.matrix.android.sdk.api.network.ssl.Fingerprint
sealed interface OnboardingAction : VectorViewModelAction { sealed interface OnboardingAction : VectorViewModelAction {
data class OnGetStarted(val onboardingFlow: OnboardingFlow) : OnboardingAction sealed interface SplashAction: OnboardingAction {
data class OnIAlreadyHaveAnAccount(val onboardingFlow: OnboardingFlow) : 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 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 { sealed class OnboardingViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OnboardingViewEvents() data class Loading(val message: CharSequence? = null) : OnboardingViewEvents()
data class Failure(val throwable: Throwable) : 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() data class RegistrationFlowResult(val flowResult: FlowResult, val isRegistrationStarted: Boolean) : OnboardingViewEvents()
object OutdatedHomeserver : 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.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.cancelCurrentOnSet import im.vector.app.core.extensions.cancelCurrentOnSet
import im.vector.app.core.extensions.configureAndStart 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.extensions.vectorStore
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider 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.FlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.Stage 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 org.matrix.android.sdk.api.session.Session
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
@ -133,8 +135,7 @@ class OnboardingViewModel @AssistedInject constructor(
override fun handle(action: OnboardingAction) { override fun handle(action: OnboardingAction) {
when (action) { when (action) {
is OnboardingAction.OnGetStarted -> handleSplashAction(action.onboardingFlow) is OnboardingAction.SplashAction -> handleSplashAction(action)
is OnboardingAction.OnIAlreadyHaveAnAccount -> handleSplashAction(action.onboardingFlow)
is OnboardingAction.UpdateUseCase -> handleUpdateUseCase(action) is OnboardingAction.UpdateUseCase -> handleUpdateUseCase(action)
OnboardingAction.ResetUseCase -> resetUseCase() OnboardingAction.ResetUseCase -> resetUseCase()
is OnboardingAction.UpdateServerType -> handleUpdateServerType(action) is OnboardingAction.UpdateServerType -> handleUpdateServerType(action)
@ -174,9 +175,9 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun handleSplashAction(onboardingFlow: OnboardingFlow) { private fun handleSplashAction(action: OnboardingAction.SplashAction) {
setState { copy(onboardingFlow = onboardingFlow) } setState { copy(onboardingFlow = action.onboardingFlow) }
continueToPageAfterSplash(onboardingFlow) continueToPageAfterSplash(action.onboardingFlow)
} }
private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) { private fun continueToPageAfterSplash(onboardingFlow: OnboardingFlow) {
@ -629,12 +630,28 @@ class OnboardingViewModel @AssistedInject constructor(
setState { copy(isLoading = true) } setState { copy(isLoading = true) }
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold( runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) }, onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) },
onFailure = { _viewEvents.post(OnboardingViewEvents.Failure(it)) } onFailure = { onAuthenticationStartError(it, trigger) }
) )
setState { copy(isLoading = false) } 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( private suspend fun onAuthenticationStartedSuccess(
trigger: OnboardingAction.HomeServerChange, trigger: OnboardingAction.HomeServerChange,
config: HomeServerConnectionConfig, config: HomeServerConnectionConfig,

View File

@ -17,9 +17,6 @@
package im.vector.app.features.onboarding.ftueauth package im.vector.app.features.onboarding.ftueauth
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -29,8 +26,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2 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 com.google.android.material.tabs.TabLayoutMediator
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
@ -44,7 +39,6 @@ import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import javax.inject.Inject import javax.inject.Inject
private const val CAROUSEL_ROTATION_DELAY_MS = 5000L private const val CAROUSEL_ROTATION_DELAY_MS = 5000L
@ -130,68 +124,14 @@ class FtueAuthSplashCarouselFragment @Inject constructor(
private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) {
val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp 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() { private fun alreadyHaveAnAccount() {
viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn)) viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn))
} }
override fun resetViewModel() { override fun resetViewModel() {
// Nothing to do // 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.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible 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.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.databinding.FragmentFtueAuthSplashBinding 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.OnboardingAction
import im.vector.app.features.onboarding.OnboardingFlow import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.failure.Failure
import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -75,34 +71,14 @@ class FtueAuthSplashFragment @Inject constructor(
private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) { private fun splashSubmit(isAlreadyHaveAccountEnabled: Boolean) {
val getStartedFlow = if (isAlreadyHaveAccountEnabled) OnboardingFlow.SignUp else OnboardingFlow.SignInSignUp 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() { private fun alreadyHaveAnAccount() {
viewModel.handle(OnboardingAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn)) viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(onboardingFlow = OnboardingFlow.SignIn))
} }
override fun resetViewModel() { override fun resetViewModel() {
// Nothing to do // 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.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat 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.R
import im.vector.app.core.extensions.getResTintedDrawable import im.vector.app.core.extensions.getResTintedDrawable
import im.vector.app.core.extensions.getTintedDrawable 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.FtueUseCase
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.themes.ThemeProvider import im.vector.app.features.themes.ThemeProvider
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import javax.inject.Inject import javax.inject.Inject
private const val DARK_MODE_ICON_BACKGROUND_ALPHA = 0.30f 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 { private fun createIcon(@ColorRes tint: Int, icon: Int, isLightMode: Boolean): Drawable {
val context = requireContext() val context = requireContext()
val alpha = when (isLightMode) { val alpha = when (isLightMode) {
true -> LIGHT_MODE_ICON_BACKGROUND_ALPHA true -> LIGHT_MODE_ICON_BACKGROUND_ALPHA
false -> DARK_MODE_ICON_BACKGROUND_ALPHA false -> DARK_MODE_ICON_BACKGROUND_ALPHA
} }
val iconBackground = context.getResTintedDrawable(R.drawable.bg_feature_icon, tint, alpha = alpha) val iconBackground = context.getResTintedDrawable(R.drawable.bg_feature_icon, tint, alpha = alpha)
val whiteLayer = context.getTintedDrawable(R.drawable.bg_feature_icon, Color.WHITE) val whiteLayer = context.getTintedDrawable(R.drawable.bg_feature_icon, Color.WHITE)
return LayerDrawable(arrayOf(whiteLayer, iconBackground, ContextCompat.getDrawable(context, icon))) 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 option = commonOption
) )
} }
OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack() OnboardingViewEvents.OnHomeserverEdited -> activity.popBackstack()
OnboardingViewEvents.OpenCombinedLogin -> onStartCombinedLogin() 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() { private fun onStartCombinedLogin() {
addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java) addRegistrationStageFragmentToBackstack(FtueAuthCombinedLoginFragment::class.java)
} }

View File

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