summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt4
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt4
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt36
-rw-r--r--android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt78
9 files changed, 124 insertions, 20 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
index 08a99a8aef..ba7e843c6d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
@@ -54,10 +54,10 @@ class AccountViewModel(
)
.toLc<Unit, AccountUiState>()
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit))
+ .onStart { viewModelScope.launch { updateAccountExpiry() } }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), Lc.Loading(Unit))
init {
- updateAccountExpiry()
verifyPurchases()
}
@@ -88,7 +88,7 @@ class AccountViewModel(
}
private fun updateAccountExpiry() {
- viewModelScope.launch { accountRepository.getAccountData() }
+ viewModelScope.launch { accountRepository.refreshAccountData() }
}
private fun verifyPurchases() {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt
index 36e6864a80..e96162c858 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModel.kt
@@ -105,7 +105,7 @@ class AddTimeViewModel(
}
private fun updateAccountExpiry() {
- viewModelScope.launch { accountRepository.getAccountData() }
+ viewModelScope.launch { accountRepository.refreshAccountData() }
}
private fun PurchaseResult.toPurchaseState() =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
index e836acb844..4ed52d4c63 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -110,12 +111,17 @@ class ConnectViewModel(
isPlayBuild = isPlayBuild,
)
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ConnectUiState.INITIAL)
+ .onStart {
+ viewModelScope.launch {
+ accountRepository.refreshAccountData(ignoreTimeout = false)
+ }
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_0000), ConnectUiState.INITIAL)
init {
viewModelScope.launch {
if (paymentUseCase.verifyPurchases().isSuccess()) {
- accountRepository.getAccountData()
+ accountRepository.refreshAccountData()
}
}
viewModelScope.launch { deviceRepository.updateDevice() }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
index ace068304d..1ac414e725 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
@@ -84,7 +84,7 @@ class OutOfTimeViewModel(
}
private suspend fun updateAccountExpiry() {
- accountRepository.getAccountData()
+ accountRepository.refreshAccountData()
}
private fun notOutOfTimeEffect() =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
index 334a360cfd..d8ea161787 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
@@ -105,7 +105,7 @@ class WelcomeViewModel(
}
private suspend fun updateAccountExpiry() {
- accountRepository.getAccountData()
+ accountRepository.refreshAccountData()
}
sealed interface UiSideEffect {
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
index 7a6b756bf5..26fc228729 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
@@ -2,9 +2,11 @@ package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
import arrow.core.right
+import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
import io.mockk.unmockkAll
import java.time.ZonedDateTime
@@ -61,7 +63,7 @@ class AccountViewModelTest {
every { mockAccountRepository.accountData } returns accountExpiryState
every { mockDeviceRepository.deviceState } returns deviceState
coEvery { mockPaymentUseCase.paymentAvailability } returns paymentAvailability
- coEvery { mockAccountRepository.getAccountData() } returns null
+ coEvery { mockAccountRepository.refreshAccountData(any()) } just Runs
viewModel =
AccountViewModel(
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt
index 7e3f966dbb..d3c95690bf 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AddTimeViewModelTest.kt
@@ -56,7 +56,7 @@ class AddTimeViewModelTest {
VerificationResult.NothingToVerify.right()
coEvery { mockPaymentUseCase.queryPaymentAvailability() } just Runs
coEvery { mockPaymentUseCase.resetPurchaseResult() } just Runs
- coEvery { mockAccountRepository.getAccountData() } returns null
+ coEvery { mockAccountRepository.refreshAccountData(any()) } just Runs
viewModel =
AddTimeViewModel(
@@ -151,7 +151,7 @@ class AddTimeViewModelTest {
purchaseResult.emit(purchaseResultData)
// Assert
- coVerify { mockAccountRepository.getAccountData() }
+ coVerify { mockAccountRepository.refreshAccountData(ignoreTimeout = true) }
}
@Test
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
index 2e922a0895..bfb1918875 100644
--- a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
@@ -1,7 +1,6 @@
package net.mullvad.mullvadvpn.lib.shared
import arrow.core.Either
-import arrow.core.raise.nullable
import java.time.ZonedDateTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -27,6 +26,7 @@ class AccountRepository(
private val deviceRepository: DeviceRepository,
val scope: CoroutineScope,
) {
+ private var lastSuccessfulAccountDataFetch: ZonedDateTime? = null
private val _mutableAccountDataCache: MutableSharedFlow<AccountData> = MutableSharedFlow()
@@ -43,7 +43,10 @@ class AccountRepository(
managementService.deviceState.map { deviceState ->
when (deviceState) {
is DeviceState.LoggedIn -> {
- managementService.getAccountData(deviceState.accountNumber).getOrNull()
+ managementService
+ .getAccountData(deviceState.accountNumber)
+ .getOrNull()
+ ?.also { lastSuccessfulAccountDataFetch = ZonedDateTime.now() }
}
DeviceState.LoggedOut,
DeviceState.Revoked -> null
@@ -72,15 +75,27 @@ class AccountRepository(
suspend fun clearAccountHistory(): Either<ClearAccountHistoryError, Unit> =
managementService.clearAccountHistory().onRight { _mutableAccountHistory.value = null }
- suspend fun getAccountData(): AccountData? = nullable {
- val deviceState = ensureNotNull(deviceRepository.deviceState.value as? DeviceState.LoggedIn)
+ /*
+ * Fetches the account data from the server, and updates the cache.
+ * Unless force is true, it will only fetch if no fetch was made in the last minute.
+ */
+ suspend fun refreshAccountData(ignoreTimeout: Boolean = true) {
+ // Only refresh if logged in
+ val deviceState = deviceRepository.deviceState.value as? DeviceState.LoggedIn ?: return
- val accountData =
- managementService.getAccountData(deviceState.accountNumber).getOrNull().bind()
+ if (ignoreTimeout || lastSuccessfulAccountDataFetch.canFetchAccountData()) {
+ val accountData =
+ managementService.getAccountData(deviceState.accountNumber).getOrNull()
+ lastSuccessfulAccountDataFetch = ZonedDateTime.now()
- // Update stateflow cache
- _mutableAccountDataCache.emit(accountData)
- accountData
+ // Update stateflow cache, only update if device state is still logged in and using the
+ // same account number
+ deviceRepository.deviceState.value?.let {
+ if (it is DeviceState.LoggedIn && it.accountNumber == accountData?.accountNumber) {
+ _mutableAccountDataCache.emit(accountData)
+ }
+ }
+ }
}
suspend fun getWebsiteAuthToken(): WebsiteAuthToken? =
@@ -93,4 +108,7 @@ class AccountRepository(
fun resetIsNewAccount() {
_isNewAccount.value = false
}
+
+ private fun ZonedDateTime?.canFetchAccountData(): Boolean =
+ this == null || this.isBefore(ZonedDateTime.now().minusMinutes(1))
}
diff --git a/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt
new file mode 100644
index 0000000000..ff2ecb88b2
--- /dev/null
+++ b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepositoryTest.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import arrow.core.right
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountNumber
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(TestCoroutineRule::class)
+class AccountRepositoryTest {
+
+ private val mockManagementService: ManagementService = mockk()
+ private val mockDeviceRepository: DeviceRepository = mockk()
+
+ private val mockDeviceStateFlow = MutableStateFlow<DeviceState>(DeviceState.LoggedOut)
+
+ private lateinit var accountRepository: AccountRepository
+
+ @BeforeEach
+ fun setup() {
+ every { mockDeviceRepository.deviceState } returns mockDeviceStateFlow
+ every { mockManagementService.deviceState } returns mockDeviceStateFlow
+
+ accountRepository =
+ AccountRepository(
+ managementService = mockManagementService,
+ deviceRepository = mockDeviceRepository,
+ scope = MainScope(),
+ )
+ }
+
+ @Test
+ fun `given force is true should always call managementService getAccountData`() = runTest {
+ // Arrange
+ val accountData: AccountData = mockk()
+ val accountNumber = AccountNumber("1234567890")
+ every { accountData.accountNumber } returns accountNumber
+ coEvery { mockManagementService.getAccountData(accountNumber) } returns accountData.right()
+
+ // Act
+ mockDeviceStateFlow.emit(DeviceState.LoggedIn(accountNumber, mockk(relaxed = true)))
+ accountRepository.refreshAccountData(ignoreTimeout = true)
+
+ // Assert
+ coVerify { mockManagementService.getAccountData(accountNumber) }
+ }
+
+ @Test
+ fun `given last latestAccountDataFetch null should always call managementService getAccountData`() =
+ runTest {
+ // Arrange
+ val accountData: AccountData = mockk()
+ val accountNumber = AccountNumber("1234567890")
+ every { accountData.accountNumber } returns accountNumber
+ coEvery { mockManagementService.getAccountData(accountNumber) } returns
+ accountData.right()
+
+ // Act
+ mockDeviceStateFlow.emit(DeviceState.LoggedIn(accountNumber, mockk(relaxed = true)))
+ accountRepository.refreshAccountData(ignoreTimeout = false)
+
+ // Assert
+ coVerify { mockManagementService.getAccountData(accountNumber) }
+ }
+}