summaryrefslogtreecommitdiffhomepage
path: root/android/app
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2024-11-13 16:32:11 +0100
committerDavid Göransson <david.goransson@mullvad.net>2024-11-18 09:07:15 +0100
commit9e463290801047a80bd68b337c33040ff99f1828 (patch)
tree5e54b56944242bfe7f5d90ee8d524a109733001c /android/app
parenta75e50e64701835f263b1ba66133891781843a35 (diff)
downloadmullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.tar.xz
mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.zip
Use ticker flow for Android system notifications
Diffstat (limited to 'android/app')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt15
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/AccountExpiryNotificationProviderTest.kt204
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCaseTest.kt25
3 files changed, 223 insertions, 21 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt
index 014f07bf35..057494f762 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCase.kt
@@ -9,7 +9,7 @@ import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL
-import net.mullvad.mullvadvpn.service.notifications.accountexpiry.expiryTickerFlow
+import net.mullvad.mullvadvpn.service.notifications.accountexpiry.AccountExpiryTicker
class AccountExpiryInAppNotificationUseCase(private val accountRepository: AccountRepository) {
@@ -18,20 +18,21 @@ class AccountExpiryInAppNotificationUseCase(private val accountRepository: Accou
accountRepository.accountData
.flatMapLatest { accountData ->
if (accountData != null) {
- expiryTickerFlow(
+ AccountExpiryTicker.tickerFlow(
expiry = accountData.expiryDate,
tickStart = ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD,
updateInterval = { ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL },
)
- .map {
- it?.let { expiresInPeriod ->
- InAppNotification.AccountExpiry(expiresInPeriod)
+ .map { tick ->
+ when (tick) {
+ AccountExpiryTicker.NotWithinThreshold -> emptyList()
+ is AccountExpiryTicker.Tick ->
+ listOf(InAppNotification.AccountExpiry(tick.expiresIn))
}
}
} else {
- flowOf(null)
+ flowOf(emptyList())
}
}
- .map(::listOfNotNull)
.distinctUntilChanged()
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/AccountExpiryNotificationProviderTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/AccountExpiryNotificationProviderTest.kt
new file mode 100644
index 0000000000..d830f407ab
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/AccountExpiryNotificationProviderTest.kt
@@ -0,0 +1,204 @@
+package net.mullvad.mullvadvpn
+
+import app.cash.turbine.test
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.lib.model.NotificationChannelId
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate.Cancel
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate.Notify
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD
+import net.mullvad.mullvadvpn.service.notifications.accountexpiry.AccountExpiryNotificationProvider
+import org.joda.time.DateTime
+import org.joda.time.Duration
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExperimentalCoroutinesApi
+@ExtendWith(TestCoroutineRule::class)
+class AccountExpiryNotificationProviderTest {
+
+ private lateinit var provider: AccountExpiryNotificationProvider
+
+ private val accountData = MutableStateFlow<AccountData?>(null)
+ private val deviceState = MutableStateFlow<DeviceState?>(null)
+ private val isNewDevice = MutableStateFlow(true)
+
+ @BeforeEach
+ fun setup() {
+ MockKAnnotations.init(this)
+
+ val accountRepository = mockk<AccountRepository>(relaxed = true)
+ every { accountRepository.accountData } returns accountData
+ every { accountRepository.isNewAccount } returns isNewDevice
+
+ val deviceRepository = mockk<DeviceRepository>(relaxed = true)
+ every { deviceRepository.deviceState } returns deviceState
+
+ provider =
+ AccountExpiryNotificationProvider(
+ channelId = NotificationChannelId("channelId"),
+ accountRepository = accountRepository,
+ deviceRepository = deviceRepository,
+ )
+
+ deviceState.value = DeviceState.LoggedIn(mockk(relaxed = true), mockk(relaxed = true))
+ isNewDevice.value = false
+ }
+
+ @AfterEach
+ fun teardown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `should not emit notification in initial state`() = runTest {
+ accountData.value = null
+ deviceState.value = null
+ isNewDevice.value = true
+ provider.notifications.test { expectNoEvents() }
+ }
+
+ @Test
+ fun `should emit notification if expiry time is shorter than expiry warning threshold`() =
+ runTest {
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusDays(1))
+ provider.notifications.test {
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `should emit cancel notification if user account is new`() = runTest {
+ isNewDevice.value = true
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusDays(1))
+ provider.notifications.test {
+ assertTrue(awaitItem() is Cancel)
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `should emit cancel notification if user account is logged out`() = runTest {
+ setIsLoggedIn(false)
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusDays(1))
+ provider.notifications.test {
+ assertTrue(awaitItem() is Cancel)
+ expectNoEvents()
+
+ setIsLoggedIn(true)
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+
+ setIsLoggedIn(false)
+ assertTrue(awaitItem() is Cancel)
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `should emit zero duration notification when remaining time runs out`() = runTest {
+ setExpiry(DateTime.now().plus(Duration.standardSeconds(60)))
+ provider.notifications.test {
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+
+ advanceTimeBy(59.seconds)
+ expectNoEvents()
+
+ advanceTimeBy(2.seconds)
+ val item = getAccountExpiry(awaitItem())
+ assertEquals(item.durationUntilExpiry, Duration.ZERO)
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `should emit notification when update interval is passed`() = runTest {
+ setExpiry(
+ DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusDays(1).plusHours(1)
+ )
+ provider.notifications.test {
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+
+ advanceTimeBy(59.minutes)
+ expectNoEvents()
+
+ advanceTimeBy(1.minutes + 1.seconds)
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `should cancel existing notification if more time is added to account`() = runTest {
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusDays(1))
+ provider.notifications.test {
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).plusDays(1))
+ assertTrue(awaitItem() is Cancel)
+ expectNoEvents()
+ }
+ }
+
+ @Test
+ fun `should not cancel existing notification if too little time is added`() = runTest {
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusDays(1))
+ provider.notifications.test {
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+
+ setExpiry(DateTime.now().plus(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD).minusHours(1))
+ assertTrue(awaitItem() is Notify)
+ expectNoEvents()
+ }
+ }
+
+ private fun getAccountExpiry(
+ awaitItem: NotificationUpdate<Notification.AccountExpiry>
+ ): Notification.AccountExpiry =
+ when (awaitItem) {
+ is Cancel -> error("expected AccountExpiry, was Cancel")
+ is Notify -> awaitItem.value
+ }
+
+ private fun setExpiry(expiryDateTime: DateTime): DateTime {
+ val expiry = AccountData(mockk(relaxed = true), expiryDateTime)
+ accountData.value = expiry
+ return expiryDateTime
+ }
+
+ private fun setIsLoggedIn(isLoggedIn: Boolean) {
+ deviceState.value =
+ if (isLoggedIn) {
+ DeviceState.LoggedIn(
+ accountNumber = mockk(relaxed = true),
+ device = mockk(relaxed = true),
+ )
+ } else {
+ DeviceState.LoggedOut
+ }
+ }
+}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCaseTest.kt
index df337a5911..316d12addd 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryInAppNotificationUseCaseTest.kt
@@ -21,7 +21,6 @@ import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL
import org.joda.time.DateTime
import org.joda.time.Duration
-import org.joda.time.Period
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -64,7 +63,7 @@ class AccountExpiryInAppNotificationUseCaseTest {
accountExpiryInAppNotificationUseCase().test {
assertTrue { awaitItem().isEmpty() }
val expiry = setExpiry(notificationThreshold.minusHours(1))
- assertExpiryNotificationAndPeriod(expiry, expectMostRecentItem())
+ assertExpiryNotificationDuration(expiry, expectMostRecentItem())
expectNoEvents()
}
}
@@ -91,17 +90,17 @@ class AccountExpiryInAppNotificationUseCaseTest {
// Advance to after threshold
advanceTimeBy(2.seconds)
- assertExpiryNotificationAndPeriod(expiry, expectMostRecentItem())
+ assertExpiryNotificationDuration(expiry, expectMostRecentItem())
expectNoEvents()
}
}
@Test
- fun `should emit zero period when the time expires`() = runTest {
+ fun `should emit zero duration when the time expires`() = runTest {
accountExpiryInAppNotificationUseCase().test {
assertTrue { awaitItem().isEmpty() }
- // Set expiry to to be in the final update period.
+ // Set expiry to to be in the final update interval.
val inLastUpdate =
DateTime.now()
.plus(ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL)
@@ -110,7 +109,7 @@ class AccountExpiryInAppNotificationUseCaseTest {
// The expiry time is within the notification threshold so we should have an item
// immediately.
- assertExpiryNotificationAndPeriod(expiry, expectMostRecentItem())
+ assertExpiryNotificationDuration(expiry, expectMostRecentItem())
expectNoEvents()
// Advance past the delay before the while loop:
@@ -132,18 +131,16 @@ class AccountExpiryInAppNotificationUseCaseTest {
return expiryDateTime
}
- // Assert that we go a single AccountExpiry notification and that the period is within
- // the expected range (checking exact period values is not possible since we use DateTime.now)
- private fun assertExpiryNotificationAndPeriod(
+ // Assert that we got a single AccountExpiry notification and that the expiry duration is within
+ // the expected range (checking exact duration value is not possible since we use DateTime.now)
+ private fun assertExpiryNotificationDuration(
expiry: DateTime,
notifications: List<InAppNotification>,
) {
val notificationDuration = getExpiryNotificationDuration(notifications)
- val periodNow = Period(DateTime.now(), expiry)
- assertTrue(periodNow.toStandardDuration() <= notificationDuration)
- assertTrue(
- periodNow.toStandardDuration().plus(Duration.standardSeconds(5)) > notificationDuration
- )
+ val expiresFromNow = Duration(DateTime.now(), expiry)
+ assertTrue(expiresFromNow <= notificationDuration)
+ assertTrue(expiresFromNow.plus(Duration.standardSeconds(5)) > notificationDuration)
}
private fun getExpiryNotificationDuration(notifications: List<InAppNotification>): Duration {