diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2021-03-31 12:31:47 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2021-03-31 12:31:47 -0300 |
| commit | b3fba9f694f573404968ae63428f2ce36966d85e (patch) | |
| tree | 436e60620b19d5876686defc7600e6288fc8cfc9 /android | |
| parent | afa8cea611744de08125ac52215cc764f4a84eed (diff) | |
| parent | e7242208e5540767d2a09189c6907b333ed85fae (diff) | |
| download | mullvadvpn-b3fba9f694f573404968ae63428f2ce36966d85e.tar.xz mullvadvpn-b3fba9f694f573404968ae63428f2ce36966d85e.zip | |
Merge branch 'split-account-cache'
Diffstat (limited to 'android')
22 files changed, 480 insertions, 276 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt index 23bc01d5a4..527082d323 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt @@ -4,17 +4,23 @@ import android.os.Message as RawMessage import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.KeygenEvent +import net.mullvad.mullvadvpn.model.LoginStatus as LoginStatusData import net.mullvad.mullvadvpn.model.Settings // Events that can be sent from the service -sealed class Event : Message() { - protected override val messageId = 1 +sealed class Event : Message.EventMessage() { protected override val messageKey = MESSAGE_KEY @Parcelize + data class AccountHistory(val history: List<String>?) : Event() + + @Parcelize object ListenerReady : Event() @Parcelize + data class LoginStatus(val status: LoginStatusData?) : Event() + + @Parcelize data class NewLocation(val location: GeoIpLocation?) : Event() @Parcelize diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt index 872acba8e7..df4811672d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt @@ -4,8 +4,10 @@ import android.os.Bundle import android.os.Message as RawMessage import android.os.Parcelable -abstract class Message : Parcelable { - protected abstract val messageId: Int +sealed class Message(private val messageId: Int) : Parcelable { + abstract class EventMessage : Message(1) + abstract class RequestMessage : Message(2) + protected abstract val messageKey: String val message: RawMessage diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt index d85090ac05..c6d03bc4b4 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt @@ -3,16 +3,34 @@ package net.mullvad.mullvadvpn.ipc import android.os.Message as RawMessage import android.os.Messenger import kotlinx.parcelize.Parcelize +import org.joda.time.DateTime // Requests that the service can handle -sealed class Request : Message() { - protected override val messageId = 2 +sealed class Request : Message.RequestMessage() { protected override val messageKey = MESSAGE_KEY @Parcelize + object CreateAccount : Request() + + @Parcelize + object FetchAccountExpiry : Request() + + @Parcelize + data class InvalidateAccountExpiry(val expiry: DateTime) : Request() + + @Parcelize + data class Login(val account: String?) : Request() + + @Parcelize + object Logout : Request() + + @Parcelize data class RegisterListener(val listener: Messenger) : Request() @Parcelize + data class RemoveAccountFromHistory(val account: String?) : Request() + + @Parcelize object WireGuardGenerateKey : Request() @Parcelize diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt new file mode 100644 index 0000000000..e143cc630c --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.joda.time.DateTime + +@Parcelize +data class LoginStatus( + val account: String, + val expiry: DateTime?, + val isNewAccount: Boolean +) : Parcelable { + val isExpired: Boolean + get() = expiry != null && expiry.isAfterNow() +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt deleted file mode 100644 index 5548b93b36..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt +++ /dev/null @@ -1,163 +0,0 @@ -package net.mullvad.mullvadvpn.service - -import kotlinx.coroutines.delay -import net.mullvad.mullvadvpn.model.GetAccountDataResult -import net.mullvad.mullvadvpn.service.endpoint.SettingsListener -import net.mullvad.mullvadvpn.util.ExponentialBackoff -import net.mullvad.mullvadvpn.util.JobTracker -import net.mullvad.talpid.util.EventNotifier -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormat - -class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsListener) { - companion object { - public val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") - - // Number of retry attempts to check for a changed expiry before giving up. - // Current value will force the cache to keep fetching for about four minutes or until a new - // expiry value is received. - // This is only used if the expiry was invalidated and fetching a new expiry returns the - // same value as before the invalidation. - private const val MAX_INVALIDATED_RETRIES = 7 - } - - val onAccountNumberChange = EventNotifier<String?>(null) - val onAccountExpiryChange = EventNotifier<DateTime?>(null) - val onAccountHistoryChange = EventNotifier<ArrayList<String>>(ArrayList()) - - var newlyCreatedAccount = false - private set - - private val jobTracker = JobTracker() - - private var accountNumber by onAccountNumberChange.notifiable() - private var accountExpiry by onAccountExpiryChange.notifiable() - private var accountHistory by onAccountHistoryChange.notifiable() - - private var createdAccountExpiry: DateTime? = null - private var oldAccountExpiry: DateTime? = null - - init { - settingsListener.accountNumberNotifier.subscribe(this) { accountNumber -> - handleNewAccountNumber(accountNumber) - } - } - - fun createNewAccount(): String? { - newlyCreatedAccount = true - createdAccountExpiry = null - - return daemon.createNewAccount() - } - - fun login(account: String) { - if (account != accountNumber) { - markAccountAsNotNew() - daemon.setAccount(account) - } - } - - fun fetchAccountExpiry() { - synchronized(this) { - accountNumber?.let { account -> - jobTracker.newBackgroundJob("fetch") { - val delays = ExponentialBackoff().apply { - cap = 2 /* h */ * 60 /* min */ * 60 /* s */ * 1000 /* ms */ - } - - do { - val result = daemon.getAccountData(account) - - if (result is GetAccountDataResult.Ok) { - val expiry = result.accountData.expiry - val retryAttempt = delays.iteration - - if (handleNewExpiry(account, expiry, retryAttempt)) { - break - } - } else if (result is GetAccountDataResult.InvalidAccount) { - break - } - - delay(delays.next()) - } while (onAccountExpiryChange.hasListeners()) - } - } - } - } - - fun invalidateAccountExpiry(accountExpiryToInvalidate: DateTime) { - synchronized(this) { - if (accountExpiry == accountExpiryToInvalidate) { - oldAccountExpiry = accountExpiryToInvalidate - fetchAccountExpiry() - } - } - } - - fun removeAccountFromHistory(accountToken: String) { - jobTracker.newBackgroundJob("removeAccountFromHistory $accountToken") { - daemon.removeAccountFromHistory(accountToken) - fetchAccountHistory() - } - } - - fun onDestroy() { - settingsListener.accountNumberNotifier.unsubscribe(this) - jobTracker.cancelAllJobs() - } - - private fun fetchAccountHistory() { - jobTracker.newBackgroundJob("fetchHistory") { - daemon.getAccountHistory()?.let { history -> - accountHistory = history - } - } - } - - private fun markAccountAsNotNew() { - newlyCreatedAccount = false - createdAccountExpiry = null - } - - private fun handleNewAccountNumber(newAccountNumber: String?) { - synchronized(this) { - accountExpiry = null - accountNumber = newAccountNumber - - fetchAccountExpiry() - fetchAccountHistory() - } - } - - private fun handleNewExpiry( - accountNumberUsedForFetch: String, - expiryString: String, - retryAttempt: Int - ): Boolean { - synchronized(this) { - if (accountNumber !== accountNumberUsedForFetch) { - return true - } - - val newAccountExpiry = DateTime.parse(expiryString, EXPIRY_FORMAT) - - if (newAccountExpiry != oldAccountExpiry || retryAttempt >= MAX_INVALIDATED_RETRIES) { - accountExpiry = newAccountExpiry - oldAccountExpiry = null - - if (accountExpiry != null && newlyCreatedAccount) { - if (createdAccountExpiry == null) { - createdAccountExpiry = accountExpiry - } else if (accountExpiry != createdAccountExpiry) { - markAccountAsNotNew() - } - } - - return true - } - - return false - } - } -} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt index 888b0b77e6..7138e0ebae 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt @@ -43,10 +43,6 @@ class ForegroundNotificationManager( } } - private var accountNumberEvents by autoSubscribable<String?>(this, null) { accountNumber -> - loggedIn = accountNumber != null - } - private var tunnelStateEvents by autoSubscribable<TunnelState>( this, TunnelState.Disconnected @@ -68,6 +64,10 @@ class ForegroundNotificationManager( private val shouldBeOnForeground get() = lockedToForeground || !(tunnelState is TunnelState.Disconnected) + var accountNumberEvents by autoSubscribable<String?>(this, null) { accountNumber -> + loggedIn = accountNumber != null + } + var onForeground = false private set @@ -77,7 +77,6 @@ class ForegroundNotificationManager( init { serviceNotifier.subscribe(this) { newServiceInstance -> - accountNumberEvents = newServiceInstance?.settingsListener?.accountNumberNotifier tunnelStateEvents = newServiceInstance?.connectionProxy?.onStateChange } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index 91bfc8118b..939d6a1d86 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -58,7 +58,7 @@ class MullvadVpnService : TalpidVpnService() { oldInstance?.onDestroy() accountExpiryNotification = newInstance?.let { instance -> - AccountExpiryNotification(this, instance.daemon, instance.accountCache) + AccountExpiryNotification(this, instance.daemon, endpoint.accountCache) } serviceNotifier.notify(newInstance) @@ -239,6 +239,8 @@ class MullvadVpnService : TalpidVpnService() { val customDns = CustomDns(daemon, endpoint.settingsListener) val splitTunneling = splitTunneling.await() + notificationManager.accountNumberEvents = endpoint.settingsListener.accountNumberNotifier + splitTunneling.onChange = { excludedApps -> disallowedApps = excludedApps markTunAsStale() @@ -256,7 +258,6 @@ class MullvadVpnService : TalpidVpnService() { daemonInstance.intermittentDaemon, connectionProxy, customDns, - endpoint.settingsListener, splitTunneling ) } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt index 251802dce9..483fbce6e5 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt @@ -1,7 +1,6 @@ package net.mullvad.mullvadvpn.service import android.os.Messenger -import net.mullvad.mullvadvpn.service.endpoint.SettingsListener import net.mullvad.mullvadvpn.util.Intermittent class ServiceInstance( @@ -10,13 +9,9 @@ class ServiceInstance( val intermittentDaemon: Intermittent<MullvadDaemon>, val connectionProxy: ConnectionProxy, val customDns: CustomDns, - val settingsListener: SettingsListener, val splitTunneling: SplitTunneling ) { - val accountCache = AccountCache(daemon, settingsListener) - fun onDestroy() { - accountCache.onDestroy() connectionProxy.onDestroy() customDns.onDestroy() } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt new file mode 100644 index 0000000000..74cdc3f11f --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt @@ -0,0 +1,276 @@ +package net.mullvad.mullvadvpn.service.endpoint + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.delay +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.Request +import net.mullvad.mullvadvpn.model.GetAccountDataResult +import net.mullvad.mullvadvpn.model.LoginStatus +import net.mullvad.mullvadvpn.util.ExponentialBackoff +import net.mullvad.mullvadvpn.util.JobTracker +import net.mullvad.talpid.util.EventNotifier +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat + +class AccountCache(private val endpoint: ServiceEndpoint) { + companion object { + public val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") + + // Number of retry attempts to check for a changed expiry before giving up. + // Current value will force the cache to keep fetching for about four minutes or until a new + // expiry value is received. + // This is only used if the expiry was invalidated and fetching a new expiry returns the + // same value as before the invalidation. + private const val MAX_INVALIDATED_RETRIES = 7 + + private sealed class Command { + object CreateAccount : Command() + data class Login(val account: String) : Command() + object Logout : Command() + } + } + + private val commandChannel = spawnActor() + + private val daemon + get() = endpoint.intermittentDaemon + + val onAccountNumberChange = EventNotifier<String?>(null) + val onAccountExpiryChange = EventNotifier<DateTime?>(null) + val onAccountHistoryChange = EventNotifier<List<String>>(listOf<String>()) + val onLoginStatusChange = EventNotifier<LoginStatus?>(null) + + var newlyCreatedAccount = false + private set + + private val jobTracker = JobTracker() + + private var accountNumber by onAccountNumberChange.notifiable() + private var accountExpiry by onAccountExpiryChange.notifiable() + private var accountHistory by onAccountHistoryChange.notifiable() + + private var createdAccountExpiry: DateTime? = null + private var oldAccountExpiry: DateTime? = null + + var loginStatus by onLoginStatusChange.notifiable() + private set + + init { + endpoint.settingsListener.accountNumberNotifier.subscribe(this) { accountNumber -> + handleNewAccountNumber(accountNumber) + } + + onAccountHistoryChange.subscribe(this) { history -> + endpoint.sendEvent(Event.AccountHistory(history)) + } + + onLoginStatusChange.subscribe(this) { status -> + endpoint.sendEvent(Event.LoginStatus(status)) + } + + endpoint.dispatcher.apply { + registerHandler(Request.CreateAccount::class) { _ -> + commandChannel.sendBlocking(Command.CreateAccount) + } + + registerHandler(Request.Login::class) { request -> + request.account?.let { account -> + commandChannel.sendBlocking(Command.Login(account)) + } + } + + registerHandler(Request.Logout::class) { _ -> + commandChannel.sendBlocking(Command.Logout) + } + + registerHandler(Request.FetchAccountExpiry::class) { _ -> + fetchAccountExpiry() + } + + registerHandler(Request.InvalidateAccountExpiry::class) { request -> + invalidateAccountExpiry(request.expiry) + } + + registerHandler(Request.RemoveAccountFromHistory::class) { request -> + request.account?.let { account -> + removeAccountFromHistory(account) + } + } + } + } + + fun onDestroy() { + endpoint.settingsListener.accountNumberNotifier.unsubscribe(this) + jobTracker.cancelAllJobs() + + onAccountNumberChange.unsubscribeAll() + onAccountExpiryChange.unsubscribeAll() + onAccountHistoryChange.unsubscribeAll() + onLoginStatusChange.unsubscribeAll() + + commandChannel.close() + } + + private fun fetchAccountExpiry() { + synchronized(this) { + accountNumber?.let { account -> + jobTracker.newBackgroundJob("fetch") { + val delays = ExponentialBackoff().apply { + cap = 2 /* h */ * 60 /* min */ * 60 /* s */ * 1000 /* ms */ + } + + do { + val result = daemon.await().getAccountData(account) + + if (result is GetAccountDataResult.Ok) { + val expiry = result.accountData.expiry + val retryAttempt = delays.iteration + + if (handleNewExpiry(account, expiry, retryAttempt)) { + break + } + } else if (result is GetAccountDataResult.InvalidAccount) { + break + } + + delay(delays.next()) + } while (onAccountExpiryChange.hasListeners()) + } + } + } + } + + private fun invalidateAccountExpiry(accountExpiryToInvalidate: DateTime) { + synchronized(this) { + if (accountExpiry == accountExpiryToInvalidate) { + oldAccountExpiry = accountExpiryToInvalidate + fetchAccountExpiry() + } + } + } + + private fun removeAccountFromHistory(accountToken: String) { + jobTracker.newBackgroundJob("removeAccountFromHistory $accountToken") { + daemon.await().removeAccountFromHistory(accountToken) + fetchAccountHistory() + } + } + + private fun spawnActor() = GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) { + try { + val command = channel.receive() + + when (command) { + is Command.CreateAccount -> doCreateAccount() + is Command.Login -> doLogin(command.account) + is Command.Logout -> doLogout() + } + } catch (exception: ClosedReceiveChannelException) { + // Command channel was closed, stop the actor + } + } + + private suspend fun doCreateAccount() { + newlyCreatedAccount = true + createdAccountExpiry = null + + daemon.await().createNewAccount() + } + + private suspend fun doLogin(account: String) { + if (account == accountNumber) { + return + } + + val result = daemon.await().getAccountData(account) + + val expiry = when (result) { + is GetAccountDataResult.Ok -> DateTime.parse(result.accountData.expiry, EXPIRY_FORMAT) + is GetAccountDataResult.RpcError -> null + else -> return + } + + synchronized(this) { + markAccountAsNotNew() + + accountNumber = account + accountExpiry = expiry + loginStatus = LoginStatus(account, expiry, false) + } + + daemon.await().setAccount(account) + } + + private suspend fun doLogout() { + if (accountNumber != null) { + daemon.await().setAccount(null) + } + } + + private fun fetchAccountHistory() { + jobTracker.newBackgroundJob("fetchHistory") { + daemon.await().getAccountHistory()?.let { history -> + accountHistory = history + } + } + } + + private fun markAccountAsNotNew() { + newlyCreatedAccount = false + createdAccountExpiry = null + } + + private fun handleNewAccountNumber(newAccountNumber: String?) { + synchronized(this) { + accountExpiry = null + accountNumber = newAccountNumber + + loginStatus = newAccountNumber?.let { account -> + LoginStatus(account, null, newlyCreatedAccount) + } + + fetchAccountExpiry() + fetchAccountHistory() + } + } + + private fun handleNewExpiry( + accountNumberUsedForFetch: String, + expiryString: String, + retryAttempt: Int + ): Boolean { + synchronized(this) { + if (accountNumber !== accountNumberUsedForFetch) { + return true + } + + val newAccountExpiry = DateTime.parse(expiryString, EXPIRY_FORMAT) + + if (newAccountExpiry != oldAccountExpiry || retryAttempt >= MAX_INVALIDATED_RETRIES) { + accountExpiry = newAccountExpiry + oldAccountExpiry = null + + loginStatus = loginStatus?.let { currentStatus -> + LoginStatus(currentStatus.account, newAccountExpiry, currentStatus.isNewAccount) + } + + if (accountExpiry != null && newlyCreatedAccount) { + if (createdAccountExpiry == null) { + createdAccountExpiry = accountExpiry + } else if (accountExpiry != createdAccountExpiry) { + markAccountAsNotNew() + } + } + + return true + } + + return false + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt index 05f903e66a..70dd295d5f 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt @@ -33,6 +33,7 @@ class ServiceEndpoint( val settingsListener = SettingsListener(this) + val accountCache = AccountCache(this) val keyStatusListener = KeyStatusListener(this) val locationInfoCache = LocationInfoCache(this) @@ -46,6 +47,7 @@ class ServiceEndpoint( dispatcher.onDestroy() registrationQueue.close() + accountCache.onDestroy() keyStatusListener.onDestroy() locationInfoCache.onDestroy() settingsListener.onDestroy() @@ -89,6 +91,8 @@ class ServiceEndpoint( listeners.add(listener) val initialEvents = listOf( + Event.LoginStatus(accountCache.onLoginStatusChange.latestEvent), + Event.AccountHistory(accountCache.onAccountHistoryChange.latestEvent), Event.SettingsUpdate(settingsListener.settings), Event.NewLocation(locationInfoCache.location), Event.WireGuardKeyStatus(keyStatusListener.keyStatus), diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt index ee82610035..8e74b8de7b 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt @@ -9,8 +9,8 @@ import android.net.Uri import kotlin.properties.Delegates.observable import kotlinx.coroutines.delay import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.service.MullvadDaemon +import net.mullvad.mullvadvpn.service.endpoint.AccountCache import net.mullvad.mullvadvpn.util.JobTracker import org.joda.time.DateTime import org.joda.time.Duration diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt index 5cb2da2152..846bf996e6 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt @@ -155,17 +155,11 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) { } private suspend fun logout() { - clearAccountNumber() + accountCache.logout() clearBackStack() goToLoginScreen() } - private suspend fun clearAccountNumber() { - jobTracker.runOnBackground { - daemon.setAccount(null) - } - } - private fun clearBackStack() { parentFragmentManager.apply { val firstEntry = getBackStackEntryAt(0) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt index ec281fd1ee..83f0ecc3ad 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt @@ -8,21 +8,20 @@ import android.view.ViewGroup import android.widget.ScrollView import android.widget.TextView import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.model.GetAccountDataResult -import net.mullvad.mullvadvpn.service.AccountCache +import net.mullvad.mullvadvpn.model.LoginStatus import net.mullvad.mullvadvpn.ui.widget.AccountLogin import net.mullvad.mullvadvpn.ui.widget.Button -import org.joda.time.DateTime class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), NavigationBarPainter { - enum class LoginResult { - ExistingAccountWithTime, - ExistingAccountOutOfTime, - NewAccount; + companion object { + private enum class State { + Starting, + Idle, + LoggingIn, + CreatingAccount, + } } private lateinit var title: TextView @@ -34,7 +33,8 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na private lateinit var scrollArea: ScrollView private lateinit var background: View - private val loggedIn = CompletableDeferred<LoginResult>() + private var loginStatus: LoginStatus? = null + private var state = State.Starting override fun onSafelyCreateView( inflater: LayoutInflater, @@ -71,29 +71,27 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na override fun onSafelyStart() { accountLogin.state = LoginState.Initial - jobTracker.newBackgroundJob("checkIfAlreadyLoggedIn") { - if (accountCache.onAccountNumberChange.latestEvent != null) { - val loginResult = if (accountCache.newlyCreatedAccount) { - LoginResult.NewAccount - } else { - loginResultForExpiry(accountCache.onAccountExpiryChange.latestEvent) - } - - loggedIn.complete(loginResult) + accountCache.onAccountHistoryChange.subscribe(this) { history -> + jobTracker.newUiJob("updateHistory") { + accountLogin.accountHistory = history } } - jobTracker.newUiJob("advanceToNextScreen") { - when (loggedIn.await()) { - LoginResult.ExistingAccountWithTime -> openNextScreen(ConnectFragment()) - LoginResult.ExistingAccountOutOfTime -> openNextScreen(OutOfTimeFragment()) - LoginResult.NewAccount -> openNextScreen(WelcomeFragment()) - } - } + accountCache.onLoginStatusChange.subscribe(this) { status -> + jobTracker.newUiJob("updateLoginStatus") { + loginStatus = status - accountCache.onAccountHistoryChange.subscribe(this) { history -> - jobTracker.newUiJob("updateHistory") { - accountLogin.accountHistory = history + if (status == null) { + if (state == State.LoggingIn || state == State.CreatingAccount) { + loginFailure() + } + } else { + if (state == State.Starting) { + openNextScreen() + } else { + loggedIn() + } + } } } @@ -105,6 +103,8 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na false } } + + state = State.Idle } override fun onResume() { @@ -115,6 +115,7 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na override fun onSafelyStop() { jobTracker.cancelJob("advanceToNextScreen") accountCache.onAccountHistoryChange.unsubscribe(this) + accountCache.onLoginStatusChange.unsubscribe(this) parentActivity.backButtonHandler = null } @@ -125,6 +126,8 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na } private suspend fun createAccount() { + state = State.CreatingAccount + title.setText(R.string.logging_in_title) subtitle.setText(R.string.creating_new_account) @@ -136,18 +139,12 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na scrollToShow(loggingInStatus) - val accountToken = jobTracker.runOnBackground { - accountCache.createNewAccount() - } - - if (accountToken == null) { - loginFailure(R.string.failed_to_create_account) - } else { - loggedIn(resources.getString(R.string.account_created), LoginResult.NewAccount) - } + accountCache.createNewAccount() } private fun login(accountToken: String) { + state = State.LoggingIn + title.setText(R.string.logging_in_title) subtitle.setText(R.string.logging_in_description) @@ -161,43 +158,18 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na scrollToShow(loggingInStatus) - performLogin(accountToken) + accountCache.login(accountToken) } - private fun performLogin(accountToken: String) { - jobTracker.newUiJob("login") { - val loginResult = jobTracker.runOnBackground { - val accountDataResult = daemon.getAccountData(accountToken) - - when (accountDataResult) { - is GetAccountDataResult.Ok -> { - accountCache.login(accountToken) - - val expiryString = accountDataResult.accountData.expiry - val expiry = DateTime.parse(expiryString, AccountCache.EXPIRY_FORMAT) - - loginResultForExpiry(expiry) - } - is GetAccountDataResult.RpcError -> { - accountCache.login(accountToken) - LoginResult.ExistingAccountWithTime - } - else -> null - } - } - - if (loginResult != null) { - loggedIn("", loginResult) - } else { - loginFailure(R.string.login_fail_description) - } + private suspend fun loggedIn() { + if (loginStatus?.isNewAccount ?: false) { + showLoggedInMessage(resources.getString(R.string.account_created)) + } else { + showLoggedInMessage("") } - } - private suspend fun loggedIn(subtitleMessage: String, result: LoginResult) { - showLoggedInMessage(subtitleMessage) delay(1000) - loggedIn.complete(result) + openNextScreen() } private fun showLoggedInMessage(subtitleMessage: String) { @@ -213,14 +185,31 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na scrollToShow(loggedInStatus) } - private fun openNextScreen(fragment: Fragment) { + private fun openNextScreen() { + val status = loginStatus + + val fragment = when { + status == null -> return + status.isNewAccount -> WelcomeFragment() + status.isExpired -> OutOfTimeFragment() + else -> ConnectFragment() + } + parentFragmentManager.beginTransaction().apply { replace(R.id.main_fragment, fragment) commit() } } - private fun loginFailure(description: Int) { + private fun loginFailure() { + val description = when (state) { + State.LoggingIn -> R.string.login_fail_description + State.CreatingAccount -> R.string.failed_to_create_account + State.Idle, State.Starting -> return + } + + state = State.Idle + title.setText(R.string.login_fail_title) subtitle.setText(description) @@ -232,12 +221,4 @@ class LoginFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen), Na scrollToShow(accountLogin) } - - private fun loginResultForExpiry(expiry: DateTime?): LoginResult { - if (expiry == null || expiry.isAfterNow()) { - return LoginResult.ExistingAccountWithTime - } else { - return LoginResult.ExistingAccountOutOfTime - } - } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt index 5822da9a91..e5936a1ffb 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt @@ -15,8 +15,8 @@ import android.widget.TextView import androidx.fragment.app.DialogFragment import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.model.VoucherSubmissionResult -import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.service.MullvadDaemon +import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache import net.mullvad.mullvadvpn.ui.widget.Button import net.mullvad.mullvadvpn.util.JobTracker import net.mullvad.mullvadvpn.util.SegmentedInputFormatter diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt index 1e892f9160..c49ac5343a 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt @@ -7,11 +7,11 @@ import android.view.ViewGroup import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoCache import net.mullvad.mullvadvpn.dataproxy.RelayListListener -import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.service.ConnectionProxy import net.mullvad.mullvadvpn.service.CustomDns import net.mullvad.mullvadvpn.service.MullvadDaemon import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache import net.mullvad.mullvadvpn.ui.serviceconnection.KeyStatusListener import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt index 5a0bb79cde..585ff11c2d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt @@ -10,7 +10,7 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.collect import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoCache -import net.mullvad.mullvadvpn.service.AccountCache +import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.mullvadvpn.ui.widget.AccountCell import net.mullvad.mullvadvpn.ui.widget.AppVersionCell diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt index c159ee9550..b45a4e53e8 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt @@ -2,8 +2,8 @@ package net.mullvad.mullvadvpn.ui.notification import android.content.Context import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.service.MullvadDaemon +import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache import net.mullvad.mullvadvpn.util.TimeLeftFormatter import org.joda.time.DateTime diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt new file mode 100644 index 0000000000..96d49df850 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt @@ -0,0 +1,67 @@ +package net.mullvad.mullvadvpn.ui.serviceconnection + +import android.os.Messenger +import net.mullvad.mullvadvpn.ipc.DispatchingHandler +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.Request +import net.mullvad.mullvadvpn.model.LoginStatus +import net.mullvad.talpid.util.EventNotifier +import org.joda.time.DateTime + +class AccountCache(val connection: Messenger, eventDispatcher: DispatchingHandler<Event>) { + val onAccountNumberChange = EventNotifier<String?>(null) + val onAccountExpiryChange = EventNotifier<DateTime?>(null) + val onAccountHistoryChange = EventNotifier<List<String>>(listOf<String>()) + val onLoginStatusChange = EventNotifier<LoginStatus?>(null) + + private var accountHistory by onAccountHistoryChange.notifiable() + private var loginStatus by onLoginStatusChange.notifiable() + + init { + eventDispatcher.apply { + registerHandler(Event.AccountHistory::class) { event -> + accountHistory = event.history ?: listOf<String>() + } + + registerHandler(Event.LoginStatus::class) { event -> + loginStatus = event.status + + onAccountNumberChange.notifyIfChanged(loginStatus?.account) + onAccountExpiryChange.notifyIfChanged(loginStatus?.expiry) + } + } + } + + fun createNewAccount() { + connection.send(Request.CreateAccount.message) + } + + fun login(account: String) { + connection.send(Request.Login(account).message) + } + + fun logout() { + connection.send(Request.Logout.message) + } + + fun fetchAccountExpiry() { + connection.send(Request.FetchAccountExpiry.message) + } + + fun invalidateAccountExpiry(accountExpiryToInvalidate: DateTime) { + val request = Request.InvalidateAccountExpiry(accountExpiryToInvalidate) + + connection.send(request.message) + } + + fun removeAccountFromHistory(account: String) { + connection.send(Request.RemoveAccountFromHistory(account).message) + } + + fun onDestroy() { + onAccountNumberChange.unsubscribeAll() + onAccountExpiryChange.unsubscribeAll() + onAccountHistoryChange.unsubscribeAll() + onLoginStatusChange.unsubscribeAll() + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt index a9ac1da761..2d2bad5553 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt @@ -22,7 +22,7 @@ class ServiceConnection(private val service: ServiceInstance, val mainActivity: } val daemon = service.daemon - val accountCache = service.accountCache + val accountCache = AccountCache(service.messenger, dispatcher) val connectionProxy = service.connectionProxy val customDns = service.customDns val keyStatusListener = KeyStatusListener(service.messenger, dispatcher) @@ -42,6 +42,7 @@ class ServiceConnection(private val service: ServiceInstance, val mainActivity: fun onDestroy() { dispatcher.onDestroy() + accountCache.onDestroy() keyStatusListener.onDestroy() locationInfoCache.onDestroy() settingsListener.onDestroy() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountHistoryAdapter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountHistoryAdapter.kt index 555d8e75b0..caefcfe51f 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountHistoryAdapter.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountHistoryAdapter.kt @@ -14,7 +14,7 @@ class AccountHistoryAdapter : Adapter<AccountHistoryHolder>() { } } - var accountHistory by observable(ArrayList<String>()) { _, _, _ -> + var accountHistory by observable(listOf<String>()) { _, _, _ -> notifyDataSetChanged() } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountLogin.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountLogin.kt index 2d608f8c67..e4256a9dc5 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountLogin.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountLogin.kt @@ -99,7 +99,7 @@ class AccountLogin : RelativeLayout { val hasFocus get() = focused - var accountHistory by observable<ArrayList<String>?>(null) { _, _, history -> + var accountHistory by observable<List<String>?>(null) { _, _, history -> val entryCount = history?.size ?: 0 historyHeight = entryCount * (historyEntryHeight + dividerHeight) diff --git a/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt b/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt index e0dc26f972..444dd54f42 100644 --- a/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt +++ b/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt @@ -26,6 +26,14 @@ class EventNotifier<T>(private val initialValue: T) { } } + fun notifyIfChanged(event: T) { + synchronized(this) { + if (latestEvent != event) { + notify(event) + } + } + } + fun subscribe(id: Any, listener: (T) -> Unit) { synchronized(this) { listeners.put(id, listener) |
