summaryrefslogtreecommitdiffhomepage
path: root/android/service/src
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/service/src
parenta75e50e64701835f263b1ba66133891781843a35 (diff)
downloadmullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.tar.xz
mullvadvpn-9e463290801047a80bd68b337c33040ff99f1828.zip
Use ticker flow for Android system notifications
Diffstat (limited to 'android/service/src')
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt1
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt1
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt71
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTicker.kt71
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTickerFlow.kt62
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