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/service/src | |
| parent | a75e50e64701835f263b1ba66133891781843a35 (diff) | |
| download | mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.tar.xz mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.zip | |
Use ticker flow for Android system notifications
Diffstat (limited to 'android/service/src')
5 files changed, 115 insertions, 91 deletions
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 |
