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 | |
| parent | a75e50e64701835f263b1ba66133891781843a35 (diff) | |
| download | mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.tar.xz mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.zip | |
Use ticker flow for Android system notifications
Diffstat (limited to 'android')
9 files changed, 342 insertions, 112 deletions
diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index ba6f2e9255..ac65d38672 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -29,6 +29,10 @@ Line wrap the file at 100 chars. Th ### Changed - Animation has been changed to look better with predictive back. +### Fixed +- Fix a bug where the Android account expiry notifications would not be updated if the app was + running in the background for a long time. + ## [android/2024.8] - 2024-11-01 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 { diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt index d6d18fd58a..db9ee92ae7 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt @@ -18,6 +18,7 @@ internal fun Notification.AccountExpiry.toNotification(context: Context) = .setContentTitle(context.resources.contentTitle(durationUntilExpiry)) .setSmallIcon(R.drawable.small_logo_white) .setOngoing(ongoing) + .setOnlyAlertOnce(true) .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) .build() diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt index d5ba20e30d..32190968b9 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt @@ -7,4 +7,5 @@ import org.joda.time.Duration val ACCOUNT_EXPIRY_POLL_INTERVAL = 15.seconds val ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL: Duration = Duration.standardDays(1) +val ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_UPDATE_INTERVAL: Duration = Duration.standardDays(1) val ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD: Duration = Duration.standardDays(3) diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt index a6a2f80d06..b513b490e0 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt @@ -1,8 +1,12 @@ package net.mullvad.mullvadvpn.service.notifications.accountexpiry +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import net.mullvad.mullvadvpn.lib.model.DeviceState import net.mullvad.mullvadvpn.lib.model.Notification import net.mullvad.mullvadvpn.lib.model.NotificationChannelId @@ -13,48 +17,57 @@ import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.service.constant.IS_PLAY_BUILD import net.mullvad.mullvadvpn.service.notifications.NotificationProvider import org.joda.time.DateTime -import org.joda.time.Duration class AccountExpiryNotificationProvider( - channelId: NotificationChannelId, - accountRepository: AccountRepository, + private val channelId: NotificationChannelId, + private val accountRepository: AccountRepository, deviceRepository: DeviceRepository, ) : NotificationProvider<Notification.AccountExpiry> { - private val notificationId = NotificationId(3) + @Suppress("MagicNumber") private val notificationId = NotificationId(3) + @OptIn(ExperimentalCoroutinesApi::class) override val notifications: Flow<NotificationUpdate<Notification.AccountExpiry>> = combine( - deviceRepository.deviceState, + deviceRepository.deviceState.filterNotNull(), accountRepository.accountData.filterNotNull(), accountRepository.isNewAccount, ) { deviceState, accountData, isNewAccount -> - if (deviceState !is DeviceState.LoggedIn) { - return@combine NotificationUpdate.Cancel(notificationId) - } - - val durationUntilExpiry = accountData.expiryDate.remainingTime() + Triple(deviceState, accountData, isNewAccount) + } + .flatMapLatest { (deviceState, accountData, isNewAccount) -> + val expiry = accountData.expiryDate - val notification = - Notification.AccountExpiry( - channelId = channelId, - actions = emptyList(), - websiteAuthToken = - if (!IS_PLAY_BUILD) accountRepository.getWebsiteAuthToken() else null, - durationUntilExpiry = durationUntilExpiry, - ) - if (!isNewAccount && durationUntilExpiry.isCloseToExpiry()) { - NotificationUpdate.Notify(notificationId, notification) + if (isNewAccount || deviceState !is DeviceState.LoggedIn) { + flowOf(NotificationUpdate.Cancel(notificationId)) } else { - NotificationUpdate.Cancel(notificationId) + accountExpiryNotificationFlow(expiry) } } - .filterNotNull() - private fun DateTime.remainingTime(): Duration { - return Duration(DateTime.now(), this) - } - - private fun Duration.isCloseToExpiry(): Boolean { - return isShorterThan(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD) - } + private fun accountExpiryNotificationFlow( + expiryDate: DateTime + ): Flow<NotificationUpdate<Notification.AccountExpiry>> = + AccountExpiryTicker.tickerFlow( + expiry = expiryDate, + tickStart = ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD, + updateInterval = { ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_UPDATE_INTERVAL }, + ) + .map { expiryTick -> + when (expiryTick) { + AccountExpiryTicker.NotWithinThreshold -> + NotificationUpdate.Cancel(notificationId) + is AccountExpiryTicker.Tick -> { + val notification = + Notification.AccountExpiry( + channelId = channelId, + actions = emptyList(), + websiteAuthToken = + if (!IS_PLAY_BUILD) accountRepository.getWebsiteAuthToken() + else null, + durationUntilExpiry = expiryTick.expiresIn, + ) + NotificationUpdate.Notify(notificationId, notification) + } + } + } } diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTicker.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTicker.kt new file mode 100644 index 0000000000..6add0a372a --- /dev/null +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTicker.kt @@ -0,0 +1,71 @@ +package net.mullvad.mullvadvpn.service.notifications.accountexpiry + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.joda.time.DateTime +import org.joda.time.Duration + +sealed interface AccountExpiryTicker { + data object NotWithinThreshold : AccountExpiryTicker + + data class Tick(val expiresIn: Duration) : AccountExpiryTicker + + companion object { + fun tickerFlow( + expiry: DateTime, + tickStart: Duration, + updateInterval: (expiry: DateTime) -> Duration, + ): Flow<AccountExpiryTicker> = flow { + expiry.millisFromNow().let { expiryMillis -> + if (expiryMillis <= 0) { + // Has expired. + emit(Tick(Duration.ZERO)) + return@flow + } + if (expiryMillis > tickStart.millis) { + // Emit NotWithinThreshold if no expiry notification should be provided. + emit(NotWithinThreshold) + // Delay until the time we should start emitting. + delay(expiryMillis - tickStart.millis + 1) + } + } + + var currentUpdateInterval = updateInterval(expiry).millis + + do { + emit(Tick(Duration(DateTime.now(), expiry))) + delay(millisUntilNextUpdate(expiry.millisFromNow(), currentUpdateInterval)) + currentUpdateInterval = updateInterval(expiry).millis + } while (hasAnotherEmission(expiry.millisFromNow(), currentUpdateInterval)) + + // We may have remaining time if the update interval wasn't a multiple of the remaining + // time. + delay(expiry.millisFromNow()) + + // We have now expired. + emit(Tick(Duration.ZERO)) + } + } +} + +private fun millisUntilNextUpdate( + millisUntilExpiry: Long, + currentUpdateIntervalMillis: Long, +): Long = + (millisUntilExpiry % currentUpdateIntervalMillis).let { + if (it == 0L) currentUpdateIntervalMillis else it + } + +private fun hasAnotherEmission(millisUntilExpiry: Long, updateIntervalMillis: Long) = + calculateDelaysNeeded(millisUntilExpiry, updateIntervalMillis) > 0 + +// Calculate how many times we need to delay and and emit until the expiry time is reached. +// Note that the returned delays may add upp to less than the remaining time, for example +// if we have 100ms remaining and currentUpdateIntervalMillis is 40ms this function will return 2. +private fun calculateDelaysNeeded( + millisUntilExpiry: Long, + currentUpdateIntervalMillis: Long, +): Long = millisUntilExpiry.coerceAtLeast(0) / currentUpdateIntervalMillis + +private fun DateTime.millisFromNow(): Long = Duration(DateTime.now(), this).millis diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTickerFlow.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTickerFlow.kt deleted file mode 100644 index 3683096c80..0000000000 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTickerFlow.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.mullvad.mullvadvpn.service.notifications.accountexpiry - -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import org.joda.time.DateTime -import org.joda.time.Duration - -fun expiryTickerFlow( - expiry: DateTime, - tickStart: Duration, - updateInterval: (expiry: DateTime) -> Duration, -): Flow<Duration?> = flow { - expiry.millisFromNow().let { expiryMillis -> - if (expiryMillis <= 0) { - // Has expired. - emit(Duration.ZERO) - return@flow - } - if (expiryMillis > tickStart.millis) { - // Emit null if no expiry notification should be provided. - emit(null) - // Delay until the time we should start emitting. - delay(expiryMillis - tickStart.millis + 1) - } - } - - var currentUpdateInterval = updateInterval(expiry).millis - - do { - emit(Duration(DateTime.now(), expiry)) - delay(millisUntilNextUpdate(expiry.millisFromNow(), currentUpdateInterval)) - currentUpdateInterval = updateInterval(expiry).millis - } while (hasAnotherEmission(expiry.millisFromNow(), currentUpdateInterval)) - - // We may have remaining time if the update interval wasn't a multiple of the remaining time. - delay(expiry.millisFromNow()) - - // We have now expired. - emit(Duration.ZERO) -} - -private fun millisUntilNextUpdate( - millisUntilExpiry: Long, - currentUpdateIntervalMillis: Long, -): Long = - (millisUntilExpiry % currentUpdateIntervalMillis).let { - if (it == 0L) currentUpdateIntervalMillis else it - } - -private fun hasAnotherEmission(millisUntilExpiry: Long, updateIntervalMillis: Long) = - calculateDelaysNeeded(millisUntilExpiry, updateIntervalMillis) > 0 - -// Calculate how many times we need to delay and and emit until the expiry time is reached. -// Note that the returned delays may add upp to less than the remaining time, for example -// if we have 100ms remaining and currentUpdateIntervalMillis is 40ms this function will return 2. -private fun calculateDelaysNeeded( - millisUntilExpiry: Long, - currentUpdateIntervalMillis: Long, -): Long = millisUntilExpiry.coerceAtLeast(0) / currentUpdateIntervalMillis - -private fun DateTime.millisFromNow(): Long = Duration(DateTime.now(), this).millis |
