summaryrefslogtreecommitdiffhomepage
path: root/android/app
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2022-05-17 15:07:03 +0200
committerAlbin <albin@mullvad.net>2022-05-17 15:07:03 +0200
commite59802548263b33bb428e5172cdb2b7833caea5f (patch)
tree549cd17a26dc041635968c049bf8d999d3c15832 /android/app
parent7f8ad3e7dfe097580e48ce2567111fb0d47962da (diff)
parent79f21dafb28dd2c0b6a41c5459c8907657fbacc5 (diff)
downloadmullvadvpn-e59802548263b33bb428e5172cdb2b7833caea5f.tar.xz
mullvadvpn-e59802548263b33bb428e5172cdb2b7833caea5f.zip
Merge branch 'refactor-android-account-handling'
Diffstat (limited to 'android/app')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt220
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LoginFragment.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt65
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt85
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt142
24 files changed, 353 insertions, 424 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt
index 7fe971e678..bbf84bf143 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt
@@ -3,11 +3,14 @@ package net.mullvad.mullvadvpn.ipc
import android.os.Message as RawMessage
import android.os.Messenger
import kotlinx.parcelize.Parcelize
+import net.mullvad.mullvadvpn.model.AccountCreationResult
+import net.mullvad.mullvadvpn.model.AccountExpiry
+import net.mullvad.mullvadvpn.model.AccountHistory
import net.mullvad.mullvadvpn.model.AppVersionInfo as AppVersionInfoData
import net.mullvad.mullvadvpn.model.DeviceState
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.LoginResult
import net.mullvad.mullvadvpn.model.RelayList
import net.mullvad.mullvadvpn.model.Settings
import net.mullvad.mullvadvpn.model.TunnelState
@@ -18,7 +21,13 @@ sealed class Event : Message.EventMessage() {
protected override val messageKey = MESSAGE_KEY
@Parcelize
- data class AccountHistory(val history: String?) : Event()
+ data class AccountCreationEvent(val result: AccountCreationResult) : Event()
+
+ @Parcelize
+ data class AccountExpiryEvent(val expiry: AccountExpiry) : Event()
+
+ @Parcelize
+ data class AccountHistoryEvent(val history: AccountHistory) : Event()
@Parcelize
data class AppVersionInfo(val versionInfo: AppVersionInfoData?) : Event()
@@ -36,7 +45,7 @@ sealed class Event : Message.EventMessage() {
data class ListenerReady(val connection: Messenger, val listenerId: Int) : Event()
@Parcelize
- data class LoginStatus(val status: LoginStatusData?) : Event()
+ data class LoginEvent(val result: LoginResult) : Event()
@Parcelize
data class NewLocation(val location: GeoIpLocation?) : Event()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt
index 78a6baa072..212f79fba4 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt
@@ -5,7 +5,6 @@ import android.os.Messenger
import java.net.InetAddress
import kotlinx.parcelize.Parcelize
import net.mullvad.mullvadvpn.model.LocationConstraint
-import org.joda.time.DateTime
// Requests that the service can handle
sealed class Request : Message.RequestMessage() {
@@ -30,13 +29,13 @@ sealed class Request : Message.RequestMessage() {
object FetchAccountExpiry : Request()
@Parcelize
- object FetchAuthToken : Request()
+ object FetchAccountHistory : Request()
@Parcelize
- data class IncludeApp(val packageName: String) : Request()
+ object FetchAuthToken : Request()
@Parcelize
- data class InvalidateAccountExpiry(val expiry: DateTime) : Request()
+ data class IncludeApp(val packageName: String) : Request()
@Parcelize
data class Login(val account: String?) : Request()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt
new file mode 100644
index 0000000000..23115b606d
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+sealed class AccountCreationResult : Parcelable {
+ @Parcelize
+ data class Success(val accountToken: String) : AccountCreationResult()
+
+ @Parcelize
+ object Failure : AccountCreationResult()
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt
new file mode 100644
index 0000000000..b057308192
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt
@@ -0,0 +1,17 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import org.joda.time.DateTime
+
+sealed class AccountExpiry : Parcelable {
+ @Parcelize
+ data class Available(val expiryDateTime: DateTime) : AccountExpiry()
+
+ @Parcelize
+ object Missing : AccountExpiry()
+
+ fun date(): DateTime? {
+ return (this as? Available)?.expiryDateTime
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt
new file mode 100644
index 0000000000..114463aaaa
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.model
+
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+
+sealed class AccountHistory : Parcelable {
+ @Parcelize
+ data class Available(val accountToken: String) : AccountHistory()
+
+ @Parcelize
+ object Missing : AccountHistory()
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt
deleted file mode 100644
index 8e3b8f841d..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginStatus.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-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,
- val loginResult: LoginResult?
-) : Parcelable {
- val isExpired: Boolean
- get() = expiry != null && expiry.isAfterNow()
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
index 649ddd65b1..a7ad35ef6d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
@@ -1,8 +1,7 @@
package net.mullvad.mullvadvpn.service
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import net.mullvad.mullvadvpn.model.AppVersionInfo
import net.mullvad.mullvadvpn.model.Device
import net.mullvad.mullvadvpn.model.DeviceEvent
@@ -34,11 +33,8 @@ class MullvadDaemon(vpnService: MullvadVpnService) {
var onRelayListChange: ((RelayList) -> Unit)? = null
var onDaemonStopped: (() -> Unit)? = null
- private val _deviceStateUpdates = MutableSharedFlow<DeviceState>(
- extraBufferCapacity = 1,
- onBufferOverflow = BufferOverflow.DROP_OLDEST
- )
- val deviceStateUpdates = _deviceStateUpdates.asSharedFlow()
+ private val _deviceStateUpdates = MutableStateFlow<DeviceState>(DeviceState.InitialState)
+ val deviceStateUpdates = _deviceStateUpdates.asStateFlow()
init {
System.loadLibrary("mullvad_jni")
@@ -125,9 +121,16 @@ class MullvadDaemon(vpnService: MullvadVpnService) {
return listDevices(daemonInterfaceAddress, accountToken)
}
- fun getDevice(): DeviceState = getDevice(daemonInterfaceAddress)
+ fun getAndEmitDeviceState(): DeviceState {
+ return getDevice(daemonInterfaceAddress).also { deviceState ->
+ _deviceStateUpdates.tryEmit(deviceState)
+ }
+ }
- fun updateDevice() = updateDevice(daemonInterfaceAddress)
+ fun refreshDevice() {
+ updateDevice(daemonInterfaceAddress)
+ getAndEmitDeviceState()
+ }
fun removeDevice(accountToken: String, deviceId: String): RemoveDeviceResult {
return removeDevice(daemonInterfaceAddress, accountToken, deviceId)
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 ea72a8379a..c1b5b47ded 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
@@ -6,13 +6,13 @@ 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 kotlinx.coroutines.flow.collect
import net.mullvad.mullvadvpn.ipc.Event
import net.mullvad.mullvadvpn.ipc.Request
+import net.mullvad.mullvadvpn.model.AccountCreationResult
+import net.mullvad.mullvadvpn.model.AccountExpiry
+import net.mullvad.mullvadvpn.model.AccountHistory
import net.mullvad.mullvadvpn.model.GetAccountDataResult
-import net.mullvad.mullvadvpn.model.LoginResult
-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
@@ -20,14 +20,7 @@ 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 val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z")
private sealed class Command {
object CreateAccount : Command()
@@ -42,36 +35,28 @@ class AccountCache(private val endpoint: ServiceEndpoint) {
get() = endpoint.intermittentDaemon
val onAccountNumberChange = EventNotifier<String?>(null)
- val onAccountExpiryChange = EventNotifier<DateTime?>(null)
- val onAccountHistoryChange = EventNotifier<String?>(null)
- val onLoginStatusChange = EventNotifier<LoginStatus?>(null)
-
- var newlyCreatedAccount = false
- private set
+ val onAccountExpiryChange = EventNotifier<AccountExpiry>(AccountExpiry.Missing)
+ val onAccountHistoryChange = EventNotifier<AccountHistory>(AccountHistory.Missing)
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)
+ jobTracker.newBackgroundJob("autoFetchAccountExpiry") {
+ daemon.await().deviceStateUpdates.collect { deviceState ->
+ accountExpiry = deviceState.token()
+ ?.let { fetchAccountExpiry(it) } ?: AccountExpiry.Missing
+ }
}
onAccountHistoryChange.subscribe(this) { history ->
- endpoint.sendEvent(Event.AccountHistory(history))
+ endpoint.sendEvent(Event.AccountHistoryEvent(history))
}
- onLoginStatusChange.subscribe(this) { status ->
- endpoint.sendEvent(Event.LoginStatus(status))
+ onAccountExpiryChange.subscribe(this) {
+ endpoint.sendEvent(Event.AccountExpiryEvent(it))
}
endpoint.dispatcher.apply {
@@ -90,15 +75,22 @@ class AccountCache(private val endpoint: ServiceEndpoint) {
}
registerHandler(Request.FetchAccountExpiry::class) { _ ->
- fetchAccountExpiry()
+ jobTracker.newBackgroundJob("fetchAccountExpiry") {
+ accountExpiry =
+ accountToken()?.let { fetchAccountExpiry(it) } ?: AccountExpiry.Missing
+ }
}
- registerHandler(Request.InvalidateAccountExpiry::class) { request ->
- invalidateAccountExpiry(request.expiry)
+ registerHandler(Request.FetchAccountHistory::class) { _ ->
+ jobTracker.newBackgroundJob("fetchAccountHistory") {
+ accountHistory = fetchAccountHistory()
+ }
}
registerHandler(Request.ClearAccountHistory::class) { _ ->
- clearAccountHistory()
+ jobTracker.newBackgroundJob("clearAccountHistory") {
+ clearAccountHistory()
+ }
}
}
}
@@ -110,54 +102,12 @@ class AccountCache(private val endpoint: ServiceEndpoint) {
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 clearAccountHistory() {
- jobTracker.newBackgroundJob("clearAccountHistory") {
- daemon.await().clearAccountHistory()
- fetchAccountHistory()
- }
+ private suspend fun accountToken(): String? {
+ return daemon.await().deviceStateUpdates.value.token()
}
private fun spawnActor() = GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
@@ -174,105 +124,61 @@ class AccountCache(private val endpoint: ServiceEndpoint) {
}
}
- private suspend fun doCreateAccount() {
- newlyCreatedAccount = true
- createdAccountExpiry = null
+ private suspend fun clearAccountHistory() {
+ daemon.await().clearAccountHistory()
+ accountHistory = fetchAccountHistory()
+ }
+ private suspend fun doCreateAccount() {
daemon.await().createNewAccount()
+ .let { newAccountNumber ->
+ if (newAccountNumber != null) {
+ AccountCreationResult.Success(newAccountNumber)
+ } else {
+ AccountCreationResult.Failure
+ }
+ }
+ .also { result ->
+ endpoint.sendEvent(Event.AccountCreationEvent(result))
+ }
}
private suspend fun doLogin(account: String) {
- val loginResult = daemon.await().loginAccount(account)
-
- val accountExpiryDate = loginResult
- .takeIf { it == LoginResult.Ok }
- .let { daemon.await().getAccountData(account) as? GetAccountDataResult.Ok }
- ?.let { DateTime.parse(it.accountData.expiry, EXPIRY_FORMAT) }
-
- synchronized(this) {
- markAccountAsNotNew()
- accountNumber = account
- accountExpiry = accountExpiryDate
-
- loginStatus = LoginStatus(
- account = account,
- expiry = accountExpiryDate,
- isNewAccount = newlyCreatedAccount,
- loginResult
- )
+ daemon.await().loginAccount(account).also { result ->
+ endpoint.sendEvent(Event.LoginEvent(result))
}
}
private suspend fun doLogout() {
daemon.await().logoutAccount()
- loginStatus = null
- fetchAccountHistory()
+ accountHistory = fetchAccountHistory()
}
- private fun fetchAccountHistory() {
- jobTracker.newBackgroundJob("fetchHistory") {
- daemon.await().getAccountHistory().let { history ->
- accountHistory = history
+ private suspend fun fetchAccountHistory(): AccountHistory {
+ return daemon.await().getAccountHistory().let { history ->
+ if (history != null) {
+ AccountHistory.Available(history)
+ } else {
+ AccountHistory.Missing
}
}
}
- 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, null)
+ private suspend fun fetchAccountExpiry(accountToken: String): AccountExpiry {
+ return fetchAccountData(accountToken).let { result ->
+ if (result is GetAccountDataResult.Ok) {
+ AccountExpiry.Available(result.parseExpiryDate())
+ } else {
+ AccountExpiry.Missing
}
-
- 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,
- null
- )
- }
-
- if (accountExpiry != null && newlyCreatedAccount) {
- if (createdAccountExpiry == null) {
- createdAccountExpiry = accountExpiry
- } else if (accountExpiry != createdAccountExpiry) {
- markAccountAsNotNew()
- }
- }
-
- return true
- }
+ private suspend fun fetchAccountData(accountToken: String): GetAccountDataResult {
+ return daemon.await().getAccountData(accountToken)
+ }
- return false
- }
+ private fun GetAccountDataResult.Ok.parseExpiryDate(): DateTime {
+ return DateTime.parse(this.accountData.expiry, EXPIRY_FORMAT)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
index a61bb15ed2..cc23b3fe01 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
@@ -30,9 +30,7 @@ class DaemonDeviceDataSource(
endpoint.dispatcher.registerHandler(Request.RefreshDeviceState::class) {
tracker.newBackgroundJob("refreshDeviceJob") {
- daemon.getDevice()
- .let { deviceState -> Event.DeviceStateEvent(deviceState) }
- .also { event -> endpoint.sendEvent(event) }
+ daemon.refreshDevice()
}
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
index f53454b401..2592ffa3ec 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
@@ -134,8 +134,7 @@ class ServiceEndpoint(
val initialEvents = mutableListOf(
Event.TunnelStateChange(connectionProxy.state),
- Event.LoginStatus(accountCache.onLoginStatusChange.latestEvent),
- Event.AccountHistory(accountCache.onAccountHistoryChange.latestEvent),
+ Event.AccountHistoryEvent(accountCache.onAccountHistoryChange.latestEvent),
Event.SettingsUpdate(settingsListener.settings),
Event.NewLocation(locationInfoCache.location),
Event.WireGuardKeyStatus(keyStatusListener.keyStatus),
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
index 7b835bccdc..5720c2eee1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
@@ -10,7 +10,7 @@ import androidx.core.app.NotificationCompat
import kotlin.properties.Delegates.observable
import kotlinx.coroutines.delay
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.model.LoginStatus
+import net.mullvad.mullvadvpn.model.AccountExpiry
import net.mullvad.mullvadvpn.service.MullvadDaemon
import net.mullvad.mullvadvpn.service.endpoint.AccountCache
import net.mullvad.mullvadvpn.util.Intermittent
@@ -44,33 +44,31 @@ class AccountExpiryNotification(
true
)
- var loginStatus by observable<LoginStatus?>(null) { _, oldValue, newValue ->
+ var accountExpiry by observable<AccountExpiry>(
+ AccountExpiry.Missing
+ ) { _, oldValue, newValue ->
if (oldValue != newValue) {
jobTracker.newUiJob("update") { update(newValue) }
}
}
init {
- accountCache.onLoginStatusChange.subscribe(this) { newStatus ->
- loginStatus = newStatus
+ accountCache.onAccountExpiryChange.subscribe(this) { expiry ->
+ accountExpiry = expiry
}
}
fun onDestroy() {
accountCache.onAccountNumberChange.unsubscribe(this)
- loginStatus = null
}
- private suspend fun update(loginStatus: LoginStatus?) {
- val remainingTime = loginStatus?.expiry?.let { expiry -> Duration(DateTime.now(), expiry) }
- val closeToExpire = remainingTime?.isShorterThan(REMAINING_TIME_FOR_REMINDERS) ?: false
- val accountIsNew = loginStatus?.isNewAccount ?: false
-
- if (closeToExpire && !accountIsNew) {
- val notification = build(loginStatus!!.expiry!!, remainingTime!!)
+ private suspend fun update(expiry: AccountExpiry) {
+ val expiryDate = expiry.date()
+ val durationUntilExpiry = expiryDate?.remainingTime()
+ if (durationUntilExpiry?.isCloseToExpiry() == true) {
+ val notification = build(expiryDate, durationUntilExpiry)
channel.notificationManager.notify(NOTIFICATION_ID, notification)
-
jobTracker.newUiJob("scheduleUpdate") { scheduleUpdate() }
} else {
channel.notificationManager.cancel(NOTIFICATION_ID)
@@ -78,9 +76,17 @@ class AccountExpiryNotification(
}
}
+ private fun DateTime.remainingTime(): Duration {
+ return Duration(DateTime.now(), this)
+ }
+
+ private fun Duration.isCloseToExpiry(): Boolean {
+ return isShorterThan(REMAINING_TIME_FOR_REMINDERS)
+ }
+
private suspend fun scheduleUpdate() {
delay(TIME_BETWEEN_CHECKS)
- update(loginStatus)
+ update(accountExpiry)
}
private suspend fun build(expiry: DateTime, remainingTime: Duration): Notification {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt
index 4b76bb2b6a..526b5356f4 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AccountFragment.kt
@@ -7,6 +7,7 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import java.text.DateFormat
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
@@ -117,11 +118,13 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
}
}
- accountCache.onAccountExpiryChange.subscribe(this) { accountExpiry ->
- jobTracker.newUiJob("updateAccountExpiry") {
- currentAccountExpiry = accountExpiry
- updateAccountExpiry(accountExpiry)
- }
+ jobTracker.newUiJob("updateAccountExpiry") {
+ accountCache.accountExpiryState
+ .map { state -> state.date() }
+ .collect { expiryDate ->
+ currentAccountExpiry = expiryDate
+ updateAccountExpiry(expiryDate)
+ }
}
connectionProxy.onUiStateChange.subscribe(this) { uiState ->
@@ -134,15 +137,11 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
}
}
- oldAccountExpiry?.let { expiry ->
- accountCache.invalidateAccountExpiry(expiry)
- }
-
sitePaymentButton.updateAuthTokenCache(authTokenCache)
+ accountCache.fetchAccountExpiry()
}
override fun onSafelyStop() {
- accountCache.onAccountExpiryChange.unsubscribe(this)
jobTracker.cancelAllJobs()
}
@@ -153,7 +152,6 @@ class AccountFragment : ServiceDependentFragment(OnNoService.GoBack) {
private fun checkForAddedTime() {
currentAccountExpiry?.let { expiry ->
oldAccountExpiry = expiry
- accountCache.invalidateAccountExpiry(expiry)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt
index 6f701c9ba7..629c5942c3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt
@@ -7,6 +7,8 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.ui.notification.AccountExpiryNotification
@@ -101,20 +103,24 @@ class ConnectFragment :
}
}
- accountCache.onAccountExpiryChange.subscribe(this) { expiry ->
- if (expiry?.isBeforeNow() ?: false) {
- openOutOfTimeScreen()
- } else if (expiry != null) {
- scheduleNextAccountExpiryCheck(expiry)
- }
+ jobTracker.newUiJob("updateAccountExpiry") {
+ accountCache.accountExpiryState
+ .map { state -> state.date() }
+ .collect { expiryDate ->
+ if (expiryDate?.isBeforeNow == true) {
+ openOutOfTimeScreen()
+ } else if (expiryDate != null)
+ scheduleNextAccountExpiryCheck(expiryDate)
+ }
}
}
override fun onSafelyStop() {
+ jobTracker.cancelJob("updateAccountExpiry")
+
locationInfoCache.onNewLocation = null
relayListListener.onRelayListChange = null
- accountCache.onAccountExpiryChange.unsubscribe(this)
keyStatusListener.onKeyStatusChange.unsubscribe(this)
connectionProxy.onUiStateChange.unsubscribe(this)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt
index f2e441d96a..9499d1c9f1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt
@@ -47,6 +47,7 @@ class LaunchFragment : ServiceAwareFragment() {
when (deviceState) {
is DeviceState.LoggedIn -> advanceToConnectScreen()
is DeviceState.LoggedOut -> advanceToLoginScreen()
+ is DeviceState.Revoked -> advanceToRevokedScreen()
else -> Unit
}
}
@@ -66,4 +67,12 @@ class LaunchFragment : ServiceAwareFragment() {
commit()
}
}
+
+ private fun advanceToRevokedScreen() {
+ // TODO: Open revoked screen.
+ parentFragmentManager.beginTransaction().apply {
+ replace(R.id.main_fragment, LoginFragment())
+ commit()
+ }
+ }
}
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 91fec93d87..056bff9227 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
@@ -16,6 +16,7 @@ import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.model.AccountHistory
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection
import net.mullvad.mullvadvpn.ui.widget.AccountLogin
import net.mullvad.mullvadvpn.ui.widget.HeaderBar
@@ -120,6 +121,7 @@ class LoginFragment :
launch {
loginViewModel.accountHistory.collect { history ->
accountLogin.accountHistory = history
+ .let { it as? AccountHistory.Available }?.accountToken
}
}
launch {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt
index 658315773e..bb65aa2135 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/OutOfTimeFragment.kt
@@ -7,6 +7,8 @@ import android.view.ViewGroup
import android.widget.TextView
import kotlin.properties.Delegates.observable
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.ui.widget.Button
@@ -72,8 +74,12 @@ class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen)
}
override fun onSafelyStart() {
- accountCache.onAccountExpiryChange.subscribe(this) { expiry ->
- checkExpiry(expiry)
+ jobTracker.newUiJob("updateAccountExpiry") {
+ accountCache.accountExpiryState
+ .map { state -> state.date() }
+ .collect { expiryDate ->
+ checkExpiry(expiryDate)
+ }
}
jobTracker.newBackgroundJob("pollAccountData") {
@@ -87,7 +93,7 @@ class OutOfTimeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen)
}
override fun onSafelyStop() {
- accountCache.onAccountExpiryChange.unsubscribe(this)
+ jobTracker.cancelJob("updateAccountExpiry")
jobTracker.cancelJob("pollAccountData")
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
index a25ac6a1d8..eb64642a45 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/RedeemVoucherDialogFragment.kt
@@ -13,6 +13,7 @@ import android.view.ViewGroup.LayoutParams
import android.widget.EditText
import android.widget.TextView
import androidx.fragment.app.DialogFragment
+import kotlinx.coroutines.flow.collect
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.VoucherSubmissionError
import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
@@ -49,19 +50,19 @@ class RedeemVoucherDialogFragment : DialogFragment() {
parentActivity = context as MainActivity
parentActivity.serviceNotifier.subscribe(this) { connection ->
- accountCache?.onAccountExpiryChange?.unsubscribe(this@RedeemVoucherDialogFragment)
-
- accountCache = connection?.accountCache?.apply {
- onAccountExpiryChange
- .subscribe(this@RedeemVoucherDialogFragment) { newAccountExpiry ->
- accountExpiry = newAccountExpiry
- }
- }
-
+ accountCache = connection?.accountCache
voucherRedeemer = connection?.voucherRedeemer
+ }
- updateRedeemButton()
+ accountCache?.apply {
+ jobTracker.newUiJob("updateExpiry") {
+ accountCache?.accountExpiryState?.collect { state ->
+ accountExpiry = state.date()
+ }
+ }
}
+
+ updateRedeemButton()
}
override fun onCreateView(
@@ -121,6 +122,7 @@ class RedeemVoucherDialogFragment : DialogFragment() {
}
override fun onDetach() {
+ jobTracker.cancelJob("updateExpiry")
parentActivity.serviceNotifier.unsubscribe(this)
super.onDetach()
@@ -143,10 +145,6 @@ class RedeemVoucherDialogFragment : DialogFragment() {
private fun handleAddedTime(timeAdded: Long) {
if (timeAdded > 0) {
- accountExpiry?.let { oldAccountExpiry ->
- accountCache?.invalidateAccountExpiry(oldAccountExpiry)
- }
-
dismiss()
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt
index dea5d900db..08afd5c59e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt
@@ -8,6 +8,7 @@ import android.widget.ImageButton
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.model.DeviceState
@@ -107,10 +108,6 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
active = false
versionInfoCache?.onUpdate = null
- accountCache?.apply {
- onAccountExpiryChange.unsubscribe(this@SettingsFragment)
- }
-
jobTracker.cancelAllJobs()
super.onStop()
@@ -123,10 +120,12 @@ class SettingsFragment : ServiceAwareFragment(), StatusBarPainter, NavigationBar
private fun configureListeners() {
accountCache?.apply {
- onAccountExpiryChange.subscribe(this@SettingsFragment) { expiry ->
- jobTracker.newUiJob("updateAccountInfo") {
- accountMenu.accountExpiry = expiry
- }
+ jobTracker.newUiJob("updateAccountExpiry") {
+ accountExpiryState
+ .map { state -> state.date() }
+ .collect { expiryDate ->
+ accountMenu.accountExpiry = expiryDate
+ }
}
fetchAccountExpiry()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt
index ad3ee9c5e9..92364fdf0f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/WelcomeFragment.kt
@@ -64,8 +64,10 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
}
}
- accountCache.onAccountExpiryChange.subscribe(this) { expiry ->
- checkExpiry(expiry)
+ jobTracker.newUiJob("checkAccountExpiry") {
+ accountCache.accountExpiryState.collect {
+ checkExpiry(it.date())
+ }
}
jobTracker.newBackgroundJob("pollAccountData") {
@@ -79,7 +81,7 @@ class WelcomeFragment : ServiceDependentFragment(OnNoService.GoToLaunchScreen) {
}
override fun onSafelyStop() {
- accountCache.onAccountExpiryChange.unsubscribe(this)
+ jobTracker.cancelJob("checkAccountExpiry")
jobTracker.cancelJob("pollAccountData")
jobTracker.cancelJob("updateAccountNumber")
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt
index b959a06607..794c372f72 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/notification/AccountExpiryNotification.kt
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.ui.notification
import android.content.Context
+import kotlinx.coroutines.flow.collect
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache
import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
@@ -20,15 +21,15 @@ class AccountExpiryNotification(
}
override fun onResume() {
- accountCache.onAccountExpiryChange.subscribe(this) { accountExpiry ->
- jobTracker.newUiJob("updateAccountExpiry") {
- updateAccountExpiry(accountExpiry)
+ jobTracker.newUiJob("updateAccountExpiry") {
+ accountCache.accountExpiryState.collect { state ->
+ updateAccountExpiry(state.date())
}
}
}
override fun onPause() {
- accountCache.onAccountExpiryChange.unsubscribe(this)
+ jobTracker.cancelJob("updateAccountExpiry")
}
private fun updateAccountExpiry(expiry: DateTime?) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt
index 6bec5c88b5..ed42b34a7d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AccountCache.kt
@@ -1,30 +1,57 @@
package net.mullvad.mullvadvpn.ui.serviceconnection
import android.os.Messenger
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
import net.mullvad.mullvadvpn.ipc.Event
import net.mullvad.mullvadvpn.ipc.EventDispatcher
import net.mullvad.mullvadvpn.ipc.Request
-import net.mullvad.mullvadvpn.model.LoginStatus
-import net.mullvad.talpid.util.EventNotifier
-import org.joda.time.DateTime
+import net.mullvad.mullvadvpn.model.AccountCreationResult
+import net.mullvad.mullvadvpn.model.AccountExpiry
+import net.mullvad.mullvadvpn.model.AccountHistory
class AccountCache(private val connection: Messenger, eventDispatcher: EventDispatcher) {
- val onAccountExpiryChange = EventNotifier<DateTime?>(null)
- val onAccountHistoryChange = EventNotifier<String?>(null)
- val onLoginStatusChange = EventNotifier<LoginStatus?>(null)
- private var accountHistory by onAccountHistoryChange.notifiable()
- private var loginStatus by onLoginStatusChange.notifiable()
+ private val _accountCreationEvents = MutableSharedFlow<AccountCreationResult>(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val accountCreationEvents = _accountCreationEvents.asSharedFlow()
+
+ private val _accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
+ val accountExpiryState = _accountExpiryState.asStateFlow()
+
+ private val _accountHistoryEvents = MutableSharedFlow<AccountHistory>(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val accountHistoryEvents = _accountHistoryEvents.asSharedFlow()
+
+ private val _loginEvents = MutableSharedFlow<Event.LoginEvent>(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val loginEvents = _loginEvents.asSharedFlow()
init {
eventDispatcher.apply {
- registerHandler(Event.AccountHistory::class) { event ->
- accountHistory = event.history
+ registerHandler(Event.AccountCreationEvent::class) { event ->
+ _accountCreationEvents.tryEmit(event.result)
+ }
+
+ registerHandler(Event.AccountExpiryEvent::class) { event ->
+ _accountExpiryState.tryEmit(event.expiry)
}
- registerHandler(Event.LoginStatus::class) { event ->
- loginStatus = event.status
- onAccountExpiryChange.notifyIfChanged(loginStatus?.expiry)
+ registerHandler(Event.AccountHistoryEvent::class) { event ->
+ _accountHistoryEvents.tryEmit(event.history)
+ }
+
+ registerHandler(Event.LoginEvent::class) { event ->
+ _loginEvents.tryEmit(event)
}
}
}
@@ -45,19 +72,11 @@ class AccountCache(private val connection: Messenger, eventDispatcher: EventDisp
connection.send(Request.FetchAccountExpiry.message)
}
- fun invalidateAccountExpiry(accountExpiryToInvalidate: DateTime) {
- val request = Request.InvalidateAccountExpiry(accountExpiryToInvalidate)
-
- connection.send(request.message)
+ fun fetchAccountHistory() {
+ connection.send(Request.FetchAccountHistory.message)
}
fun clearAccountHistory() {
connection.send(Request.ClearAccountHistory.message)
}
-
- fun onDestroy() {
- onAccountExpiryChange.unsubscribeAll()
- onAccountHistoryChange.unsubscribeAll()
- onLoginStatusChange.unsubscribeAll()
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt
index bea6c34ecb..7c44013bca 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt
@@ -61,7 +61,6 @@ class ServiceConnection(
closeScope()
dispatcher.onDestroy()
- accountCache.onDestroy()
authTokenCache.onDestroy()
connectionProxy.onDestroy()
keyStatusListener.onDestroy()
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
index 55079458fa..cb4f151fd1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
@@ -1,12 +1,17 @@
package net.mullvad.mullvadvpn.viewmodel
import android.app.Application
-import androidx.annotation.RestrictTo
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.model.AccountCreationResult
+import net.mullvad.mullvadvpn.model.AccountHistory
import net.mullvad.mullvadvpn.model.LoginResult
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache
@@ -14,9 +19,10 @@ 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 val _accountHistory = MutableStateFlow<AccountHistory>(AccountHistory.Missing)
+ val accountHistory: StateFlow<AccountHistory> = _accountHistory
private var accountCache: AccountCache? = null
@@ -38,8 +44,15 @@ class LoginViewModel(
// 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() }
+ accountCache = newAccountCache?.apply {
+ viewModelScope.launch {
+ accountHistoryEvents.collect {
+ _accountHistory.value = it
+ }
+ }
+
+ fetchAccountHistory()
+ }
}
fun clearAccountHistory() {
@@ -47,50 +60,44 @@ class LoginViewModel(
}
fun createAccount() {
- _uiState.value = LoginUiState.CreatingAccount
- accountCache?.createNewAccount()
+ accountCache?.apply {
+ _uiState.value = LoginUiState.CreatingAccount
+
+ viewModelScope.launch {
+ _uiState.value = accountCreationEvents.first().mapToUiState()
+ }
+
+ createNewAccount()
+ }
}
fun login(accountToken: String) {
- _uiState.value = LoginUiState.Loading
- accountCache?.login(accountToken)
- }
+ accountCache?.apply {
+ _uiState.value = LoginUiState.Loading
- @RestrictTo(RestrictTo.Scope.TESTS)
- public override fun onCleared() {
- accountCache?.unsubscribe()
- }
+ viewModelScope.launch {
+ _uiState.value = loginEvents.first().result.mapToUiState()
+ }
- private fun AccountCache.subscribe() {
- onAccountHistoryChange.subscribe(this) { history ->
- _accountHistory.value = history
+ login(accountToken)
}
+ }
- 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 AccountCreationResult.mapToUiState(): LoginUiState {
+ return if (this is AccountCreationResult.Success) {
+ LoginUiState.AccountCreated
+ } else {
+ LoginUiState.UnableToCreateAccountError
}
}
- private fun AccountCache.unsubscribe() {
- onAccountHistoryChange.unsubscribe(this)
- onLoginStatusChange.unsubscribe(this)
+ private fun LoginResult.mapToUiState(): LoginUiState {
+ return when (this) {
+ LoginResult.Ok -> LoginUiState.Success(false)
+ LoginResult.InvalidAccount -> LoginUiState.InvalidAccountError
+ LoginResult.MaxDevicesReached -> LoginUiState.TooManyDevicesError
+ else -> LoginUiState.OtherError(errorMessage = this.toString())
+ }
}
class Factory(val application: Application) :
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
index 174c378e23..2a615fcc72 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
@@ -3,20 +3,21 @@ package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.FlowTurbine
import app.cash.turbine.test
import io.mockk.MockKAnnotations
-import io.mockk.Runs
import io.mockk.every
import io.mockk.impl.annotations.MockK
-import io.mockk.invoke
-import io.mockk.just
import io.mockk.mockk
-import io.mockk.slot
import io.mockk.verify
import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
+import kotlinx.coroutines.test.setMain
+import net.mullvad.mullvadvpn.ipc.Event
+import net.mullvad.mullvadvpn.model.AccountCreationResult
+import net.mullvad.mullvadvpn.model.AccountHistory
import net.mullvad.mullvadvpn.model.LoginResult
-import net.mullvad.mullvadvpn.model.LoginStatus
import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache
-import net.mullvad.talpid.util.EventNotifier
import org.junit.Before
import org.junit.Test
@@ -25,37 +26,20 @@ class LoginViewModelTest {
@MockK
private lateinit var mockedAccountCache: AccountCache
- @MockK
- private lateinit var mockedLoginStatusNotifier: EventNotifier<LoginStatus?>
-
- @MockK
- private lateinit var mockedAccountHistoryNotifier: EventNotifier<String?>
-
private lateinit var loginViewModel: LoginViewModel
- private val capturedLoginStatusNotifierCallback = slot<(LoginStatus?) -> Unit>()
- private val capturedAccountHistoryNotifierCallback = slot<(String?) -> Unit>()
+
+ private val accountCreationTestEvents = MutableSharedFlow<AccountCreationResult>()
+ private val accountHistoryTestEvents = MutableSharedFlow<AccountHistory>()
+ private val loginTestEvents = MutableSharedFlow<Event.LoginEvent>()
@Before
fun setup() {
+ Dispatchers.setMain(TestCoroutineDispatcher())
MockKAnnotations.init(this, relaxUnitFun = true)
- every {
- mockedLoginStatusNotifier.subscribe(
- any(),
- any(),
- capture(capturedLoginStatusNotifierCallback)
- )
- } just Runs
-
- every {
- mockedAccountHistoryNotifier.subscribe(
- any(),
- capture(capturedAccountHistoryNotifierCallback)
- )
- } just Runs
-
- every { mockedAccountCache.onLoginStatusChange } returns mockedLoginStatusNotifier
- every { mockedAccountCache.onAccountHistoryChange } returns mockedAccountHistoryNotifier
+ every { mockedAccountCache.accountCreationEvents } returns accountCreationTestEvents
+ every { mockedAccountCache.accountHistoryEvents } returns accountHistoryTestEvents
+ every { mockedAccountCache.loginEvents } returns loginTestEvents
loginViewModel = LoginViewModel(mockk())
}
@@ -69,23 +53,13 @@ class LoginViewModelTest {
}
@Test
- fun testClearingViewModel() {
- loginViewModel.updateAccountCacheInstance(mockedAccountCache)
- loginViewModel.onCleared()
- verify {
- mockedLoginStatusNotifier.unsubscribe(any())
- mockedAccountHistoryNotifier.unsubscribe(any())
- }
- }
-
- @Test
fun testCreateAccount() = runBlockingTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.uiState.test {
skipDefaultItem()
loginViewModel.createAccount()
assertEquals(LoginViewModel.LoginUiState.CreatingAccount, awaitItem())
- capturedLoginStatusNotifierCallback.captured.invoke(DummyLoginStatus.ACCOUNT_CREATED)
+ accountCreationTestEvents.emit(AccountCreationResult.Success(DUMMY_ACCOUNT_TOKEN))
assertEquals(LoginViewModel.LoginUiState.AccountCreated, awaitItem())
}
}
@@ -95,9 +69,9 @@ class LoginViewModelTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.uiState.test {
skipDefaultItem()
- loginViewModel.login("")
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
assertEquals(LoginViewModel.LoginUiState.Loading, awaitItem())
- capturedLoginStatusNotifierCallback.captured.invoke(DummyLoginStatus.SUCCESSFUL_LOGIN)
+ loginTestEvents.emit(Event.LoginEvent(LoginResult.Ok))
assertEquals(LoginViewModel.LoginUiState.Success(isOutOfTime = false), awaitItem())
}
}
@@ -107,11 +81,9 @@ class LoginViewModelTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.uiState.test {
skipDefaultItem()
- loginViewModel.login("")
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
assertEquals(LoginViewModel.LoginUiState.Loading, awaitItem())
- capturedLoginStatusNotifierCallback.captured.invoke(
- DummyLoginStatus.INVALID_ACCOUNT_ERROR
- )
+ loginTestEvents.emit(Event.LoginEvent(LoginResult.InvalidAccount))
assertEquals(LoginViewModel.LoginUiState.InvalidAccountError, awaitItem())
}
}
@@ -121,9 +93,9 @@ class LoginViewModelTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.uiState.test {
skipDefaultItem()
- loginViewModel.login("")
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
assertEquals(LoginViewModel.LoginUiState.Loading, awaitItem())
- capturedLoginStatusNotifierCallback.captured.invoke(DummyLoginStatus.MAX_DEVICES_ERROR)
+ loginTestEvents.emit(Event.LoginEvent(LoginResult.MaxDevicesReached))
assertEquals(LoginViewModel.LoginUiState.TooManyDevicesError, awaitItem())
}
}
@@ -133,10 +105,13 @@ class LoginViewModelTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.uiState.test {
skipDefaultItem()
- loginViewModel.login("")
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
assertEquals(LoginViewModel.LoginUiState.Loading, awaitItem())
- capturedLoginStatusNotifierCallback.captured.invoke(DummyLoginStatus.RPC_ERROR)
- assertEquals(LoginViewModel.LoginUiState.OtherError("RpcError"), awaitItem())
+ loginTestEvents.emit(Event.LoginEvent(LoginResult.RpcError))
+ assertEquals(
+ LoginViewModel.LoginUiState.OtherError(EXPECTED_RPC_ERROR_MESSAGE),
+ awaitItem()
+ )
}
}
@@ -145,10 +120,13 @@ class LoginViewModelTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.uiState.test {
skipDefaultItem()
- loginViewModel.login("")
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
assertEquals(LoginViewModel.LoginUiState.Loading, awaitItem())
- capturedLoginStatusNotifierCallback.captured.invoke(DummyLoginStatus.OTHER_ERROR)
- assertEquals(LoginViewModel.LoginUiState.OtherError("OtherError"), awaitItem())
+ loginTestEvents.emit(Event.LoginEvent(LoginResult.OtherError))
+ assertEquals(
+ LoginViewModel.LoginUiState.OtherError(EXPECTED_OTHER_ERROR_MESSAGE),
+ awaitItem()
+ )
}
}
@@ -156,8 +134,10 @@ class LoginViewModelTest {
fun testAccountHistory() = runBlockingTest {
loginViewModel.updateAccountCacheInstance(mockedAccountCache)
loginViewModel.accountHistory.test { skipDefaultItem() }
- capturedAccountHistoryNotifierCallback.invoke(DUMMY_ACCOUNT_TOKEN)
- loginViewModel.accountHistory.test { assertEquals(DUMMY_ACCOUNT_TOKEN, awaitItem()) }
+ accountHistoryTestEvents.emit(AccountHistory.Available(DUMMY_ACCOUNT_TOKEN))
+ loginViewModel.accountHistory.test {
+ assertEquals(AccountHistory.Available(DUMMY_ACCOUNT_TOKEN), awaitItem())
+ }
}
@Test
@@ -173,49 +153,7 @@ class LoginViewModelTest {
companion object {
private const val DUMMY_ACCOUNT_TOKEN = "DUMMY"
-
- private object DummyLoginStatus {
- val ACCOUNT_CREATED = LoginStatus(
- DUMMY_ACCOUNT_TOKEN,
- mockk(),
- isNewAccount = true,
- mockk()
- )
-
- val SUCCESSFUL_LOGIN = LoginStatus(
- DUMMY_ACCOUNT_TOKEN,
- mockk(),
- isNewAccount = false,
- LoginResult.Ok
- )
-
- val INVALID_ACCOUNT_ERROR = LoginStatus(
- DUMMY_ACCOUNT_TOKEN,
- mockk(),
- isNewAccount = false,
- LoginResult.InvalidAccount
- )
-
- val MAX_DEVICES_ERROR = LoginStatus(
- DUMMY_ACCOUNT_TOKEN,
- mockk(),
- isNewAccount = false,
- LoginResult.MaxDevicesReached
- )
-
- val RPC_ERROR = LoginStatus(
- DUMMY_ACCOUNT_TOKEN,
- mockk(),
- isNewAccount = false,
- LoginResult.RpcError
- )
-
- val OTHER_ERROR = LoginStatus(
- DUMMY_ACCOUNT_TOKEN,
- mockk(),
- isNewAccount = false,
- LoginResult.OtherError
- )
- }
+ private const val EXPECTED_RPC_ERROR_MESSAGE = "RpcError"
+ private const val EXPECTED_OTHER_ERROR_MESSAGE = "OtherError"
}
}