summaryrefslogtreecommitdiffhomepage
path: root/android/service/src
diff options
context:
space:
mode:
Diffstat (limited to 'android/service/src')
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt39
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiry.kt30
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt17
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt11
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt89
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/InAppAccountExpiryTicker.kt (renamed from android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryTicker.kt)8
-rw-r--r--android/service/src/test/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationTriggerTest.kt71
7 files changed, 140 insertions, 125 deletions
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt
index c33b8ef518..f5ddd24578 100644
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt
@@ -1,59 +1,20 @@
package net.mullvad.mullvadvpn.service.di
-import androidx.core.app.NotificationManagerCompat
-import kotlinx.coroutines.MainScope
import net.mullvad.mullvadvpn.lib.common.constant.CACHE_DIR_NAMED_ARGUMENT
import net.mullvad.mullvadvpn.lib.common.constant.FILES_DIR_NAMED_ARGUMENT
import net.mullvad.mullvadvpn.lib.common.constant.GRPC_SOCKET_FILE_NAMED_ARGUMENT
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointOverride
-import net.mullvad.mullvadvpn.lib.model.NotificationChannel
import net.mullvad.mullvadvpn.service.BuildConfig
import net.mullvad.mullvadvpn.service.DaemonConfig
import net.mullvad.mullvadvpn.service.migration.MigrateSplitTunneling
-import net.mullvad.mullvadvpn.service.notifications.NotificationChannelFactory
-import net.mullvad.mullvadvpn.service.notifications.NotificationManager
-import net.mullvad.mullvadvpn.service.notifications.NotificationProvider
-import net.mullvad.mullvadvpn.service.notifications.accountexpiry.AccountExpiryNotificationProvider
-import net.mullvad.mullvadvpn.service.notifications.tunnelstate.TunnelStateNotificationProvider
import org.koin.android.ext.koin.androidContext
-import org.koin.core.module.dsl.createdAtStart
-import org.koin.core.module.dsl.withOptions
import org.koin.core.qualifier.named
-import org.koin.dsl.bind
import org.koin.dsl.module
val vpnServiceModule = module {
- single { NotificationManagerCompat.from(androidContext()) }
- single { androidContext().resources }
single(named(FILES_DIR_NAMED_ARGUMENT)) { androidContext().filesDir }
single(named(CACHE_DIR_NAMED_ARGUMENT)) { androidContext().cacheDir }
- single { NotificationChannel.TunnelUpdates } bind NotificationChannel::class
- single { NotificationChannel.AccountUpdates } bind NotificationChannel::class
- single { NotificationChannelFactory(get(), get(), getAll()) } withOptions { createdAtStart() }
-
- single {
- TunnelStateNotificationProvider(
- get(),
- get(),
- get(),
- get<NotificationChannel.TunnelUpdates>().id,
- MainScope(),
- )
- } bind NotificationProvider::class
- single {
- AccountExpiryNotificationProvider(
- get<NotificationChannel.AccountUpdates>().id,
- get(),
- get(),
- )
- } bind NotificationProvider::class
-
- single { NotificationManager(get(), getAll(), get(), MainScope()) } withOptions
- {
- createdAtStart()
- }
-
single { MigrateSplitTunneling(androidContext()) }
single {
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiry.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiry.kt
new file mode 100644
index 0000000000..d5303e3903
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiry.kt
@@ -0,0 +1,30 @@
+@file:Suppress("MagicNumber")
+
+package net.mullvad.mullvadvpn.service.notifications.accountexpiry
+
+import java.time.Duration
+import java.time.ZonedDateTime
+import kotlin.time.Duration.Companion.seconds
+
+val ACCOUNT_EXPIRY_POLL_INTERVAL = 15.seconds
+
+// When to start showing the account expiry in-app notification.
+val ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD: Duration = Duration.ofDays(3)
+
+// How often to update the account expiry in-app notification.
+val ACCOUNT_EXPIRY_NOTIFICATION_UPDATE_INTERVAL: Duration = Duration.ofDays(1)
+
+// Calculate when the alarm that triggers the account expiry notification should be set.
+fun accountExpiryNotificationTriggerAt(now: ZonedDateTime, expiry: ZonedDateTime): ZonedDateTime {
+ val untilExpiry = Duration.between(now, expiry)
+
+ return if (untilExpiry > ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD) {
+ val wait = untilExpiry - ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD
+ now + wait
+ } else {
+ val wait = untilExpiry.toMillis() % ACCOUNT_EXPIRY_NOTIFICATION_UPDATE_INTERVAL.toMillis()
+
+ // If the expiry is in the past we just return it as it is.
+ if (wait >= 0) now + Duration.ofMillis(wait) else expiry
+ }
+}
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 a61561c8cc..97212797cd 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
@@ -8,7 +8,6 @@ import androidx.core.app.NotificationCompat
import java.time.Duration
import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
-import net.mullvad.mullvadvpn.lib.common.util.createAccountUri
import net.mullvad.mullvadvpn.lib.model.Notification
import net.mullvad.mullvadvpn.service.R
@@ -22,18 +21,12 @@ internal fun Notification.AccountExpiry.toNotification(context: Context) =
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.build()
-private fun Notification.AccountExpiry.contentIntent(context: Context): PendingIntent {
-
+private fun contentIntent(context: Context): PendingIntent {
val intent =
- if (websiteAuthToken == null) {
- Intent().apply {
- setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
- flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- action = Intent.ACTION_MAIN
- }
- } else {
- val uri = createAccountUri(context.getString(R.string.account_url), websiteAuthToken)
- Intent(Intent.ACTION_VIEW, uri)
+ Intent().apply {
+ setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
+ flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ action = Intent.ACTION_MAIN
}
return PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
}
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
deleted file mode 100644
index 4ee9c6a533..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryConstant.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-@file:Suppress("MagicNumber")
-
-package net.mullvad.mullvadvpn.service.notifications.accountexpiry
-
-import java.time.Duration
-import kotlin.time.Duration.Companion.seconds
-
-val ACCOUNT_EXPIRY_POLL_INTERVAL = 15.seconds
-val ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL: Duration = Duration.ofDays(1)
-val ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_UPDATE_INTERVAL: Duration = Duration.ofDays(1)
-val ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD: Duration = Duration.ofDays(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 be0141c3f4..776ce47960 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,73 +1,44 @@
package net.mullvad.mullvadvpn.service.notifications.accountexpiry
-import java.time.ZonedDateTime
-import kotlinx.coroutines.ExperimentalCoroutinesApi
+import co.touchlab.kermit.Logger
+import java.time.Duration
+import kotlinx.coroutines.channels.Channel
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 kotlinx.coroutines.flow.receiveAsFlow
import net.mullvad.mullvadvpn.lib.model.Notification
import net.mullvad.mullvadvpn.lib.model.NotificationChannelId
import net.mullvad.mullvadvpn.lib.model.NotificationId
import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
-import net.mullvad.mullvadvpn.lib.shared.AccountRepository
-import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
-import net.mullvad.mullvadvpn.service.constant.IS_PLAY_BUILD
import net.mullvad.mullvadvpn.service.notifications.NotificationProvider
-class AccountExpiryNotificationProvider(
- private val channelId: NotificationChannelId,
- private val accountRepository: AccountRepository,
- deviceRepository: DeviceRepository,
-) : NotificationProvider<Notification.AccountExpiry> {
- @Suppress("MagicNumber") private val notificationId = NotificationId(3)
+class AccountExpiryNotificationProvider(private val channelId: NotificationChannelId) :
+ NotificationProvider<Notification.AccountExpiry> {
- @OptIn(ExperimentalCoroutinesApi::class)
- override val notifications: Flow<NotificationUpdate<Notification.AccountExpiry>> =
- combine(
- deviceRepository.deviceState.filterNotNull(),
- accountRepository.accountData.filterNotNull(),
- accountRepository.isNewAccount,
- ) { deviceState, accountData, isNewAccount ->
- Triple(deviceState, accountData, isNewAccount)
- }
- .flatMapLatest { (deviceState, accountData, isNewAccount) ->
- val expiry = accountData.expiryDate
+ private val notificationChannel: Channel<NotificationUpdate<Notification.AccountExpiry>> =
+ Channel(Channel.CONFLATED)
- if (isNewAccount || deviceState !is DeviceState.LoggedIn) {
- flowOf(NotificationUpdate.Cancel(notificationId))
- } else {
- accountExpiryNotificationFlow(expiry)
- }
- }
+ override val notifications: Flow<NotificationUpdate<Notification.AccountExpiry>>
+ get() = notificationChannel.receiveAsFlow()
- private fun accountExpiryNotificationFlow(
- expiryDate: ZonedDateTime
- ): Flow<NotificationUpdate<Notification.AccountExpiry>> =
- AccountExpiryTicker.tickerFlow(
- expiry = expiryDate,
- tickStart = ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD,
- updateInterval = { ACCOUNT_EXPIRY_SYSTEM_NOTIFICATION_UPDATE_INTERVAL },
+ suspend fun showNotification(durationUntilExpiry: Duration) {
+ val notification =
+ Notification.AccountExpiry(
+ channelId = channelId,
+ actions = emptyList(),
+ durationUntilExpiry = durationUntilExpiry,
)
- .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)
- }
- }
- }
+
+ val notificationUpdate = NotificationUpdate.Notify(NOTIFICATION_ID, notification)
+ notificationChannel.send(notificationUpdate)
+ }
+
+ suspend fun cancelNotification() {
+ Logger.d("Cancelling existing account expiry notification")
+ val notificationUpdate = NotificationUpdate.Cancel(NOTIFICATION_ID)
+ notificationChannel.send(notificationUpdate)
+ }
+
+ companion object {
+ private val NOTIFICATION_ID = NotificationId(3)
+ }
}
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/InAppAccountExpiryTicker.kt
index cd872eee18..b26263f416 100644
--- 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/InAppAccountExpiryTicker.kt
@@ -8,17 +8,17 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import net.mullvad.mullvadvpn.lib.common.util.millisFromNow
-sealed interface AccountExpiryTicker {
- data object NotWithinThreshold : AccountExpiryTicker
+sealed interface InAppAccountExpiryTicker {
+ data object NotWithinThreshold : InAppAccountExpiryTicker
- data class Tick(val expiresIn: Duration) : AccountExpiryTicker
+ data class Tick(val expiresIn: Duration) : InAppAccountExpiryTicker
companion object {
fun tickerFlow(
expiry: ZonedDateTime,
tickStart: Duration,
updateInterval: (expiry: ZonedDateTime) -> Duration,
- ): Flow<AccountExpiryTicker> = flow {
+ ): Flow<InAppAccountExpiryTicker> = flow {
expiry.millisFromNow().let { expiryMillis ->
if (expiryMillis <= 0) {
// Has expired.
diff --git a/android/service/src/test/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationTriggerTest.kt b/android/service/src/test/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationTriggerTest.kt
new file mode 100644
index 0000000000..ff4340f168
--- /dev/null
+++ b/android/service/src/test/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationTriggerTest.kt
@@ -0,0 +1,71 @@
+package net.mullvad.mullvadvpn.service.notifications.accountexpiry
+
+import java.time.Duration
+import java.time.ZonedDateTime
+import kotlin.test.assertEquals
+import org.junit.jupiter.api.Test
+
+class AccountExpiryNotificationTriggerTest {
+
+ @Test
+ fun `long account expiry should trigger 3 days before expiry`() {
+ val now = ZonedDateTime.now()
+
+ val threeMonthsExpiry = now.plusDays(90)
+ val trigger1 = accountExpiryNotificationTriggerAt(now, threeMonthsExpiry)
+ assertEquals(87, Duration.between(now, trigger1).toDays())
+
+ val fourAndHalfDaysExpiry = now.plusDays(4).plusHours(12)
+ val trigger2 = accountExpiryNotificationTriggerAt(now, fourAndHalfDaysExpiry)
+ assertEquals(Duration.ofDays(1).plusHours(12), Duration.between(now, trigger2))
+ }
+
+ @Test
+ fun `account expiry that more than 2 days but less than 3 days should trigger 2 days before expiry`() {
+ val now = ZonedDateTime.now()
+ val expiry = now.plusHours(50)
+ val trigger = accountExpiryNotificationTriggerAt(now, expiry)
+ // Because acc
+ assertEquals(2, Duration.between(now, trigger).toHours())
+ }
+
+ @Test
+ fun `account expiry that is more than 1 day but less than 2 days should trigger 1 day before expiry`() {
+ val now = ZonedDateTime.now()
+ val expiry = now.plusHours(36).plusMinutes(20).plusSeconds(7)
+ val trigger = accountExpiryNotificationTriggerAt(now, expiry)
+ assertEquals(
+ Duration.ofHours(12).plusMinutes(20).plusSeconds(7),
+ Duration.between(now, trigger),
+ )
+ }
+
+ @Test
+ fun `account expiry that is less than 24 hours should trigger when account expires`() {
+ val now = ZonedDateTime.now()
+ val expiry = now.plusHours(2).plusMinutes(1).plusSeconds(30)
+ val trigger = accountExpiryNotificationTriggerAt(now, expiry)
+
+ assertEquals(
+ Duration.ofHours(2).plusMinutes(1).plusSeconds(30),
+ Duration.between(now, trigger),
+ )
+ }
+
+ @Test
+ fun `account that expires now should return now`() {
+ val now = ZonedDateTime.now()
+ val trigger = accountExpiryNotificationTriggerAt(now, now)
+
+ assertEquals(Duration.ofMillis(0), Duration.between(now, trigger))
+ }
+
+ @Test
+ fun `account expiry that is in the past should return the account expiry date`() {
+ val now = ZonedDateTime.now()
+ val expiry = now.minusDays(1).minusHours(17).minusMinutes(3).minusSeconds(40)
+ val trigger = accountExpiryNotificationTriggerAt(now, expiry)
+
+ assertEquals(expiry, trigger)
+ }
+}