diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2024-11-13 16:32:11 +0100 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2024-11-18 09:07:15 +0100 |
| commit | 9e463290801047a80bd68b337c33040ff99f1828 (patch) | |
| tree | 5e54b56944242bfe7f5d90ee8d524a109733001c /android/app | |
| parent | a75e50e64701835f263b1ba66133891781843a35 (diff) | |
| download | mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.tar.xz mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.zip | |
Use ticker flow for Android system notifications
Diffstat (limited to 'android/app')
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 { |
