diff options
| author | Albin <albin@mullvad.net> | 2022-04-01 15:23:05 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2022-04-20 16:36:51 +0200 |
| commit | 23f205bc2dc8985ff9a83d752ec011213072a77e (patch) | |
| tree | 3cbd4eb35a32511d9d55ff4a7e2e081c40c0737b /android | |
| parent | 3eb6f68172d0b54a699e13fac176dc58687c3f53 (diff) | |
| download | mullvadvpn-23f205bc2dc8985ff9a83d752ec011213072a77e.tar.xz mullvadvpn-23f205bc2dc8985ff9a83d752ec011213072a77e.zip | |
Refactor login view to use a view model
Diffstat (limited to 'android')
3 files changed, 224 insertions, 114 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt index 435c6bd220..ea72a8379a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt @@ -206,6 +206,7 @@ class AccountCache(private val endpoint: ServiceEndpoint) { private suspend fun doLogout() { daemon.await().logoutAccount() loginStatus = null + fetchAccountHistory() } private fun fetchAccountHistory() { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt index 4444812179..91fec93d87 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt @@ -8,22 +8,24 @@ import android.view.ViewGroup import android.widget.ScrollView import android.widget.TextView import androidx.core.content.ContextCompat -import kotlinx.coroutines.delay +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.model.LoginResult -import net.mullvad.mullvadvpn.model.LoginStatus +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.mullvadvpn.ui.widget.AccountLogin -import net.mullvad.mullvadvpn.ui.widget.Button +import net.mullvad.mullvadvpn.ui.widget.HeaderBar +import net.mullvad.mullvadvpn.viewmodel.LoginViewModel -class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), NavigationBarPainter { - companion object { - private enum class State { - Starting, - Idle, - LoggingIn, - CreatingAccount, - } - } +class LoginFragment : + ServiceDependentFragment(OnNoService.GoToLaunchScreen), + NavigationBarPainter { + + private lateinit var loginViewModel: LoginViewModel private lateinit var title: TextView private lateinit var subtitle: TextView @@ -33,9 +35,7 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na private lateinit var accountLogin: AccountLogin private lateinit var scrollArea: ScrollView private lateinit var background: View - - private var loginStatus: LoginStatus? = null - private var state = State.Starting + private lateinit var headerBar: HeaderBar override fun onSafelyCreateView( inflater: LayoutInflater, @@ -44,19 +44,25 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na ): View { val view = inflater.inflate(R.layout.login, container, false) + headerBar = view.findViewById(R.id.header_bar) title = view.findViewById(R.id.title) subtitle = view.findViewById(R.id.subtitle) loggingInStatus = view.findViewById(R.id.logging_in_status) loggedInStatus = view.findViewById(R.id.logged_in_status) loginFailStatus = view.findViewById(R.id.login_fail_status) + val factory = LoginViewModel.Factory(requireActivity().application) + loginViewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java].apply { + updateAccountCacheInstance(accountCache) + } + accountLogin = view.findViewById<AccountLogin>(R.id.account_login).apply { - onLogin = { accountToken -> login(accountToken) } - onClearHistory = { -> accountCache.clearAccountHistory() } + onLogin = loginViewModel::login + onClearHistory = loginViewModel::clearAccountHistory } - view.findViewById<Button>(R.id.create_account) - .setOnClickAction("createAccount", jobTracker) { createAccount() } + view.findViewById<net.mullvad.mullvadvpn.ui.widget.Button>(R.id.create_account) + .setOnClickAction("createAccount", jobTracker, loginViewModel::createAccount) scrollArea = view.findViewById(R.id.scroll_area) @@ -69,33 +75,26 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na return view } - override fun onSafelyStart() { - accountLogin.state = LoginState.Initial + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupLifecycleSubscriptionsToViewModel() + } - accountCache.onAccountHistoryChange.subscribe(this) { history -> - jobTracker.newUiJob("updateHistory") { - accountLogin.accountHistory = history - } + override fun onNewServiceConnection(serviceConnection: ServiceConnection) { + super.onNewServiceConnection(serviceConnection) + if (this::loginViewModel.isInitialized) { + loginViewModel.updateAccountCacheInstance(accountCache) } + } - accountCache.onLoginStatusChange.subscribe(this, false) { status -> - jobTracker.newUiJob("updateLoginStatus") { - loginStatus = status - - if (status == null && state == State.CreatingAccount) { - loginFailure(null) - } else if (status?.loginResult != LoginResult.Ok) { - loginFailure(status?.loginResult) - } else { - if (state == State.Starting) { - openNextScreen() - } else { - loggedIn() - } - } - } + override fun onNoServiceConnection() { + super.onNoServiceConnection() + if (this::loginViewModel.isInitialized) { + loginViewModel.updateAccountCacheInstance(null) } + } + override fun onSafelyStart() { parentActivity.backButtonHandler = { if (accountLogin.hasFocus) { background.requestFocus() @@ -104,8 +103,6 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na false } } - - state = State.Idle } override fun onResume() { @@ -114,37 +111,86 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na } override fun onSafelyStop() { - jobTracker.cancelJob("advanceToNextScreen") - accountCache.onAccountHistoryChange.unsubscribe(this) - accountCache.onLoginStatusChange.unsubscribe(this) parentActivity.backButtonHandler = null } - private fun scrollToShow(view: View) { - val rectangle = Rect(0, 0, view.width, view.height) - - scrollArea.requestChildRectangleOnScreen(view, rectangle, false) + private fun setupLifecycleSubscriptionsToViewModel() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + launch { + loginViewModel.accountHistory.collect { history -> + accountLogin.accountHistory = history + } + } + launch { + loginViewModel.uiState.collect { uiState -> updateUi(uiState) } + } + } + } } - private suspend fun createAccount() { - state = State.CreatingAccount + private fun updateUi(uiState: LoginViewModel.LoginUiState) { + when (uiState) { + is LoginViewModel.LoginUiState.Default -> { + showDefault() + } - title.setText(R.string.logging_in_title) - subtitle.setText(R.string.creating_new_account) + is LoginViewModel.LoginUiState.Success -> { + openFragment( + if (uiState.isOutOfTime) { + OutOfTimeFragment() + } else { + ConnectFragment() + } + ) + } - loggingInStatus.visibility = View.VISIBLE - loginFailStatus.visibility = View.GONE - loggedInStatus.visibility = View.GONE + is LoginViewModel.LoginUiState.AccountCreated -> { + openFragment(WelcomeFragment()) + } - accountLogin.state = LoginState.InProgress + is LoginViewModel.LoginUiState.CreatingAccount -> { + showCreatingAccount() + } - scrollToShow(loggingInStatus) + is LoginViewModel.LoginUiState.Loading -> { + showLoading() + } + + is LoginViewModel.LoginUiState.InvalidAccountError -> { + loginFailure(resources.getString(R.string.login_fail_description)) + } + + is LoginViewModel.LoginUiState.TooManyDevicesError -> { + // TODO: Switch to TooManyDevicesFragment + loginFailure("Too many devices!") + } + + is LoginViewModel.LoginUiState.UnableToCreateAccountError -> { + loginFailure(resources.getString(R.string.failed_to_create_account)) + } - accountCache.createNewAccount() + is LoginViewModel.LoginUiState.OtherError -> { + loginFailure(uiState.errorMessage) + } + } } - private fun login(accountToken: String) { - state = State.LoggingIn + private fun openFragment(fragment: Fragment) { + parentFragmentManager.beginTransaction().apply { + replace(R.id.main_fragment, fragment) + commit() + } + } + + private fun showDefault() { + accountLogin.state = LoginState.Initial + headerBar.tunnelState = null + paintNavigationBar(ContextCompat.getColor(requireContext(), R.color.darkBlue)) + } + + private fun showLoading() { + accountLogin.state = LoginState.InProgress title.setText(R.string.logging_in_title) subtitle.setText(R.string.logging_in_description) @@ -158,65 +204,22 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na accountLogin.state = LoginState.InProgress scrollToShow(loggingInStatus) - - accountCache.login(accountToken) } - private suspend fun loggedIn() { - if (loginStatus?.isNewAccount ?: false) { - showLoggedInMessage(resources.getString(R.string.account_created)) - } else { - showLoggedInMessage("") - } - - delay(1000) - openNextScreen() - } - - private fun showLoggedInMessage(subtitleMessage: String) { - title.setText(R.string.logged_in_title) - subtitle.setText(subtitleMessage) + private fun showCreatingAccount() { + title.setText(R.string.logging_in_title) + subtitle.setText(R.string.creating_new_account) - loggingInStatus.visibility = View.GONE + loggingInStatus.visibility = View.VISIBLE loginFailStatus.visibility = View.GONE - loggedInStatus.visibility = View.VISIBLE - - accountLogin.state = LoginState.Success - - scrollToShow(loggedInStatus) - } - - private fun openNextScreen() { - val status = loginStatus + loggedInStatus.visibility = View.GONE - val fragment = when { - status == null -> return - status.isNewAccount -> WelcomeFragment() - status.isExpired -> OutOfTimeFragment() - else -> ConnectFragment() - } + accountLogin.state = LoginState.InProgress - parentFragmentManager.beginTransaction().apply { - replace(R.id.main_fragment, fragment) - commit() - } + scrollToShow(loggingInStatus) } - // TODO: This error handling and its messages will change once a VM is introduced in a later - // commit related to the ongoing device adaption. - private fun loginFailure(loginResult: LoginResult?) { - val description = when { - loginResult == LoginResult.MaxDevicesReached -> "Too many devices" - loginResult == LoginResult.RpcError -> "An error occurred" - loginResult == LoginResult.OtherError -> "An error occurred" - loginResult == LoginResult.InvalidAccount -> - resources.getText(R.string.login_fail_description) - state == State.CreatingAccount -> resources.getText(R.string.failed_to_create_account) - else -> return - } - - state = State.Idle - + private fun loginFailure(description: String) { title.setText(R.string.login_fail_title) subtitle.setText(description) @@ -228,4 +231,9 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na scrollToShow(accountLogin) } + + private fun scrollToShow(view: View) { + val rectangle = Rect(0, 0, view.width, view.height) + scrollArea.requestChildRectangleOnScreen(view, rectangle, false) + } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt new file mode 100644 index 0000000000..e40620f1f0 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt @@ -0,0 +1,101 @@ +package net.mullvad.mullvadvpn.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import net.mullvad.mullvadvpn.model.LoginResult +import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache + +class LoginViewModel( + application: Application +) : AndroidViewModel(application) { + private val _uiState = MutableStateFlow<LoginUiState>(LoginUiState.Default) + private val _accountHistory = MutableStateFlow<String?>(null) + val uiState: StateFlow<LoginUiState> = _uiState + val accountHistory: StateFlow<String?> = _accountHistory + + private var accountCache: AccountCache? = null + + sealed class LoginUiState { + object Default : LoginUiState() + object Loading : LoginUiState() + data class Success( + val isOutOfTime: Boolean + ) : LoginUiState() + + object CreatingAccount : LoginUiState() + object AccountCreated : LoginUiState() + object UnableToCreateAccountError : LoginUiState() + object InvalidAccountError : LoginUiState() + object TooManyDevicesError : LoginUiState() + data class OtherError(val errorMessage: String) : LoginUiState() + } + + // Ensures the view model has an up-to-date instance of account cache. This is an intermediate + // solution due to limitations in the current app architecture. + fun updateAccountCacheInstance(newAccountCache: AccountCache?) { + accountCache?.unsubscribe() + accountCache = newAccountCache?.apply { subscribe() } + } + + fun clearAccountHistory() { + accountCache?.clearAccountHistory() + } + + fun createAccount() { + _uiState.value = LoginUiState.CreatingAccount + accountCache?.createNewAccount() + } + + fun login(accountToken: String) { + _uiState.value = LoginUiState.Loading + accountCache?.login(accountToken) + } + + override fun onCleared() { + accountCache?.onAccountHistoryChange?.unsubscribe(this) + accountCache?.onLoginStatusChange?.unsubscribe(this) + } + + private fun AccountCache.subscribe() { + onAccountHistoryChange.subscribe(this) { history -> + _accountHistory.value = history + } + + onLoginStatusChange.subscribe(this, startWithLatestEvent = false) { status -> + _uiState.value = when { + status == null -> { + LoginUiState.Default + } + status.isNewAccount -> { + LoginUiState.AccountCreated + } + else -> { + when (status.loginResult) { + LoginResult.Ok -> LoginUiState.Success(false) + LoginResult.InvalidAccount -> LoginUiState.InvalidAccountError + LoginResult.MaxDevicesReached -> LoginUiState.TooManyDevicesError + else -> LoginUiState.OtherError( + errorMessage = status.loginResult?.toString() ?: "" + ) + } + } + } + } + } + + private fun AccountCache.unsubscribe() { + onAccountHistoryChange.unsubscribe(this) + onLoginStatusChange.unsubscribe(this) + } + + class Factory(val application: Application) : + ViewModelProvider.Factory { + override fun <T : ViewModel> create(modelClass: Class<T>): T { + return LoginViewModel(application) as T + } + } +} |
