summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2022-04-01 15:23:05 +0200
committerAlbin <albin@mullvad.net>2022-04-20 16:36:51 +0200
commit23f205bc2dc8985ff9a83d752ec011213072a77e (patch)
tree3cbd4eb35a32511d9d55ff4a7e2e081c40c0737b /android
parent3eb6f68172d0b54a699e13fac176dc58687c3f53 (diff)
downloadmullvadvpn-23f205bc2dc8985ff9a83d752ec011213072a77e.tar.xz
mullvadvpn-23f205bc2dc8985ff9a83d752ec011213072a77e.zip
Refactor login view to use a view model
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt236
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt101
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
+ }
+ }
+}