summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt10
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt6
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt22
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt15
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt163
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt9
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt5
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt5
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt276
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt8
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt141
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt67
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt3
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountHistoryAdapter.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/AccountLogin.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt8
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)