diff options
| author | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-06-17 11:34:20 -0300 |
|---|---|---|
| committer | Janito Vaqueiro Ferreira Filho <janito@mullvad.net> | 2020-06-17 11:34:20 -0300 |
| commit | 914a81c77e8050ee5a241ad32d00831e6069bcae (patch) | |
| tree | 35bcb052358d47fcec7fcbeb94a021b10ead1640 /android/src | |
| parent | c53a2ff991061e6eaa2f0a45efa93a5e7a3508d5 (diff) | |
| parent | 8e0c2a39a38d8c5c41a0bd87e3f70e7b106ec3cc (diff) | |
| download | mullvadvpn-914a81c77e8050ee5a241ad32d00831e6069bcae.tar.xz mullvadvpn-914a81c77e8050ee5a241ad32d00831e6069bcae.zip | |
Merge branch 'refactor-android-notification'
Diffstat (limited to 'android/src')
8 files changed, 286 insertions, 249 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt index f63ca84f58..36e09b1721 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/AccountCache.kt @@ -20,25 +20,16 @@ class AccountCache(val daemon: MullvadDaemon, val settingsListener: SettingsList public val EXPIRY_FORMAT = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss z") } + val onAccountNumberChange = EventNotifier<String?>(null) + val onAccountExpiryChange = EventNotifier<DateTime?>(null) + private val jobTracker = JobTracker() - private var accountNumber: String? = null - set(value) { - field = value - onAccountNumberChange.notify(value) - } - - private var accountExpiry: DateTime? = null - set(value) { - field = value - onAccountExpiryChange.notify(value) - } + private var accountNumber by onAccountNumberChange.notifiable() + private var accountExpiry by onAccountExpiryChange.notifiable() private var oldAccountExpiry: DateTime? = null - val onAccountNumberChange = EventNotifier<String?>(null) - val onAccountExpiryChange = EventNotifier<DateTime?>(null) - init { settingsListener.accountNumberNotifier.subscribe(this) { accountNumber -> handleNewAccountNumber(accountNumber) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt index 28d32c1c89..5312af91f9 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt @@ -1,37 +1,24 @@ package net.mullvad.mullvadvpn.service import android.app.KeyguardManager -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build -import android.support.v4.app.NotificationCompat -import net.mullvad.mullvadvpn.R +import kotlin.properties.Delegates.observable import net.mullvad.mullvadvpn.model.TunnelState -import net.mullvad.mullvadvpn.ui.MainActivity -import net.mullvad.talpid.tunnel.ActionAfterDisconnect +import net.mullvad.mullvadvpn.service.notifications.TunnelStateNotification import net.mullvad.talpid.util.EventNotifier - -val CHANNEL_ID = "vpn_tunnel_status" -val FOREGROUND_NOTIFICATION_ID: Int = 1 -val KEY_CONNECT_ACTION = "net.mullvad.mullvadvpn.connect_action" -val KEY_DISCONNECT_ACTION = "net.mullvad.mullvadvpn.disconnect_action" +import net.mullvad.talpid.util.autoSubscribable class ForegroundNotificationManager( val service: MullvadVpnService, val serviceNotifier: EventNotifier<ServiceInstance?>, val keyguardManager: KeyguardManager ) { - private val notificationManager = - service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - private val badgeColor = service.resources.getColor(R.color.colorPrimary) + private val tunnelStateNotification = TunnelStateNotification(service) private val deviceLockListener = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -43,177 +30,39 @@ class ForegroundNotificationManager( } } - private var connectionProxy: ConnectionProxy? = null - set(value) { - if (field != value) { - field?.onStateChange?.unsubscribe(this) - - value?.onStateChange?.subscribe(this) { state -> - tunnelState = state - } + private var accountNumberEvents by autoSubscribable<String?>(this, null) { accountNumber -> + loggedIn = accountNumber != null + } - field = value - } + private var tunnelStateEvents + by autoSubscribable<TunnelState>(this, TunnelState.Disconnected()) { newState -> + tunnelStateNotification.tunnelState = newState + updateNotification() } - private var settingsListener: SettingsListener? = null - set(value) { - if (field != value) { - field?.accountNumberNotifier?.unsubscribe(this) + private var deviceIsUnlocked by observable(!keyguardManager.isDeviceLocked) { _, _, _ -> + updateNotificationAction() + } - value?.accountNumberNotifier?.subscribe(this) { accountNumber -> - loggedIn = accountNumber != null - } - - field = value - } - } + private var loggedIn by observable(false) { _, _, _ -> updateNotificationAction() } private var onForeground = false - private var reconnecting = false - private var showingReconnecting = false - - private var tunnelState: TunnelState = TunnelState.Disconnected() - set(value) { - field = value - - reconnecting = - (value is TunnelState.Disconnecting && - value.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) || - (value is TunnelState.Connecting && reconnecting) - - updateNotification() - } - - private var deviceIsUnlocked = true - set(value) { - if (field != value) { - field = value - updateNotification() - } - } - private var loggedIn = false - set(value) { - field = value - updateNotification() - } + private val tunnelState + get() = tunnelStateEvents?.latestEvent ?: TunnelState.Disconnected() private val shouldBeOnForeground get() = lockedToForeground || !(tunnelState is TunnelState.Disconnected) - private val notificationText: Int - get() { - val state = tunnelState - - return when (state) { - is TunnelState.Disconnected -> R.string.unsecured - is TunnelState.Connecting -> { - if (reconnecting) { - R.string.reconnecting - } else { - R.string.connecting - } - } - is TunnelState.Connected -> R.string.secured - is TunnelState.Disconnecting -> { - when (state.actionAfterDisconnect) { - ActionAfterDisconnect.Reconnect -> R.string.reconnecting - else -> R.string.disconnecting - } - } - is TunnelState.Error -> { - if (state.errorState.isBlocking) { - R.string.blocking_all_connections - } else { - R.string.critical_error - } - } - } - } - - private val tunnelActionText: Int - get() { - val state = tunnelState - - return when (state) { - is TunnelState.Disconnected -> R.string.connect - is TunnelState.Connecting -> R.string.cancel - is TunnelState.Connected -> R.string.disconnect - is TunnelState.Disconnecting -> { - when (state.actionAfterDisconnect) { - ActionAfterDisconnect.Reconnect -> R.string.cancel - else -> R.string.connect - } - } - is TunnelState.Error -> { - if (state.errorState.isBlocking) { - R.string.disconnect - } else { - R.string.dismiss - } - } - } - } - - private val tunnelActionKey: String - get() { - val state = tunnelState - - return when (state) { - is TunnelState.Disconnected -> KEY_CONNECT_ACTION - is TunnelState.Connecting -> KEY_DISCONNECT_ACTION - is TunnelState.Connected -> KEY_DISCONNECT_ACTION - is TunnelState.Disconnecting -> { - when (state.actionAfterDisconnect) { - ActionAfterDisconnect.Reconnect -> KEY_DISCONNECT_ACTION - else -> KEY_CONNECT_ACTION - } - } - is TunnelState.Error -> KEY_DISCONNECT_ACTION - } - } - - private val tunnelActionIcon: Int - get() { - if (tunnelActionKey == KEY_CONNECT_ACTION) { - return R.drawable.icon_notification_connect - } else { - return R.drawable.icon_notification_disconnect - } - } - - private val connectReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - connectionProxy?.connect() - } - } - - private val disconnectReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - connectionProxy?.disconnect() - } - } - - var lockedToForeground = false - set(value) { - field = value - updateNotificationForegroundStatus() - } + var lockedToForeground by observable(false) { _, _, _ -> updateNotification() } init { - if (Build.VERSION.SDK_INT >= 26) { - initChannel() - } - serviceNotifier.subscribe(this) { newServiceInstance -> - connectionProxy = newServiceInstance?.connectionProxy - settingsListener = newServiceInstance?.settingsListener + accountNumberEvents = newServiceInstance?.settingsListener?.accountNumberNotifier + tunnelStateEvents = newServiceInstance?.connectionProxy?.onStateChange } service.apply { - registerReceiver(connectReceiver, IntentFilter(KEY_CONNECT_ACTION)) - registerReceiver(disconnectReceiver, IntentFilter(KEY_DISCONNECT_ACTION)) registerReceiver(deviceLockListener, IntentFilter().apply { addAction(Intent.ACTION_USER_PRESENT) addAction(Intent.ACTION_SCREEN_OFF) @@ -225,40 +74,23 @@ class ForegroundNotificationManager( fun onDestroy() { serviceNotifier.unsubscribe(this) - connectionProxy = null - settingsListener = null - service.apply { - unregisterReceiver(connectReceiver) - unregisterReceiver(disconnectReceiver) - } + accountNumberEvents = null + tunnelStateEvents = null - notificationManager.cancel(FOREGROUND_NOTIFICATION_ID) - } + service.unregisterReceiver(deviceLockListener) - private fun initChannel() { - val channelName = service.getString(R.string.foreground_notification_channel_name) - val importance = NotificationManager.IMPORTANCE_MIN - val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply { - description = service.getString(R.string.foreground_notification_channel_description) - setShowBadge(true) - } - - notificationManager.createNotificationChannel(channel) + tunnelStateNotification.visible = false } private fun updateNotification() { - if (!reconnecting || !showingReconnecting) { - notificationManager.notify(FOREGROUND_NOTIFICATION_ID, buildNotification()) - } - - updateNotificationForegroundStatus() - } - - private fun updateNotificationForegroundStatus() { if (shouldBeOnForeground != onForeground) { if (shouldBeOnForeground) { - service.startForeground(FOREGROUND_NOTIFICATION_ID, buildNotification()) + service.startForeground( + TunnelStateNotification.NOTIFICATION_ID, + tunnelStateNotification.build() + ) + onForeground = true } else if (!shouldBeOnForeground) { if (Build.VERSION.SDK_INT >= 24) { @@ -272,40 +104,7 @@ class ForegroundNotificationManager( } } - private fun buildNotification(): Notification { - val intent = Intent(service, MainActivity::class.java) - .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) - .setAction(Intent.ACTION_MAIN) - - val pendingIntent = - PendingIntent.getActivity(service, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT) - - val builder = NotificationCompat.Builder(service, CHANNEL_ID) - .setSmallIcon(R.drawable.small_logo_black) - .setColor(badgeColor) - .setContentTitle(service.getString(notificationText)) - .setContentIntent(pendingIntent) - - if (loggedIn && deviceIsUnlocked) { - builder.addAction(buildTunnelAction()) - } - - return builder.build() - } - - private fun buildTunnelAction(): NotificationCompat.Action { - val intent = Intent(tunnelActionKey).setPackage("net.mullvad.mullvadvpn") - val flags = PendingIntent.FLAG_UPDATE_CURRENT - - val pendingIntent = if (Build.VERSION.SDK_INT >= 26) { - PendingIntent.getForegroundService(service, 1, intent, flags) - } else { - PendingIntent.getService(service, 1, intent, flags) - } - - val icon = tunnelActionIcon - val label = service.getString(tunnelActionText) - - return NotificationCompat.Action(icon, label, pendingIntent) + private fun updateNotificationAction() { + tunnelStateNotification.showAction = loggedIn && deviceIsUnlocked } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt index 4a9562c6ec..f216e69726 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt @@ -57,9 +57,9 @@ class MullvadTileService : TileService() { val intent = Intent(this, MullvadVpnService::class.java) if (secured) { - intent.action = KEY_DISCONNECT_ACTION + intent.action = MullvadVpnService.KEY_DISCONNECT_ACTION } else { - intent.action = KEY_CONNECT_ACTION + intent.action = MullvadVpnService.KEY_CONNECT_ACTION } if (Build.VERSION.SDK_INT >= 26) { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index 6952494f04..0998ddcc1c 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -23,6 +23,9 @@ private const val RELAYS_FILE = "relays.json" class MullvadVpnService : TalpidVpnService() { companion object { private val TAG = "mullvad" + + val KEY_CONNECT_ACTION = "net.mullvad.mullvadvpn.connect_action" + val KEY_DISCONNECT_ACTION = "net.mullvad.mullvadvpn.disconnect_action" } private enum class PendingAction { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt new file mode 100644 index 0000000000..2546c8955f --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt @@ -0,0 +1,61 @@ +package net.mullvad.mullvadvpn.service.notifications + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import android.support.v4.app.NotificationCompat +import net.mullvad.mullvadvpn.R + +class NotificationChannel( + val context: Context, + val id: String, + val name: Int, + val description: Int, + val importance: Int +) { + private val badgeColor by lazy { + context.resources.getColor(R.color.colorPrimary) + } + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + init { + if (Build.VERSION.SDK_INT >= 26) { + val channelName = context.getString(name) + val channelDescription = context.getString(description) + + val channel = NotificationChannel(id, channelName, importance).apply { + description = channelDescription + setShowBadge(true) + } + + notificationManager.createNotificationChannel(channel) + } + } + + fun buildNotification(intent: PendingIntent, title: Int): Notification { + return buildNotification(intent, title, emptyList()) + } + + fun buildNotification( + pendingIntent: PendingIntent, + title: Int, + actions: List<NotificationCompat.Action> + ): Notification { + val builder = NotificationCompat.Builder(context, id) + .setSmallIcon(R.drawable.small_logo_black) + .setColor(badgeColor) + .setContentTitle(context.getString(title)) + .setContentIntent(pendingIntent) + + for (action in actions) { + builder.addAction(action) + } + + return builder.build() + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt new file mode 100644 index 0000000000..e91908f088 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt @@ -0,0 +1,115 @@ +package net.mullvad.mullvadvpn.service.notifications + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.support.v4.app.NotificationCompat +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.ui.MainActivity +import net.mullvad.talpid.tunnel.ActionAfterDisconnect + +class TunnelStateNotification(val context: Context) { + companion object { + val NOTIFICATION_ID: Int = 1 + } + + private val channel = NotificationChannel( + context, + "vpn_tunnel_status", + R.string.foreground_notification_channel_name, + R.string.foreground_notification_channel_description, + NotificationManager.IMPORTANCE_MIN + ) + + private val notificationText: Int + get() = when (val state = tunnelState) { + is TunnelState.Disconnected -> R.string.unsecured + is TunnelState.Connecting -> { + if (reconnecting) { + R.string.reconnecting + } else { + R.string.connecting + } + } + is TunnelState.Connected -> R.string.secured + is TunnelState.Disconnecting -> { + when (state.actionAfterDisconnect) { + ActionAfterDisconnect.Reconnect -> R.string.reconnecting + else -> R.string.disconnecting + } + } + is TunnelState.Error -> { + if (state.errorState.isBlocking) { + R.string.blocking_all_connections + } else { + R.string.critical_error + } + } + } + + private var reconnecting = false + private var showingReconnecting = false + + var showAction by observable(false) { _, _, _ -> update() } + + var tunnelState by observable<TunnelState>(TunnelState.Disconnected()) { _, _, newState -> + reconnecting = + (newState is TunnelState.Disconnecting && + newState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) || + (newState is TunnelState.Connecting && reconnecting) + + update() + } + + var visible by observable(true) { _, _, newValue -> + if (newValue == true) { + update() + } else { + channel.notificationManager.cancel(NOTIFICATION_ID) + } + } + + private fun update() { + if (visible && (!reconnecting || !showingReconnecting)) { + channel.notificationManager.notify(NOTIFICATION_ID, build()) + } + } + + fun build(): Notification { + val intent = Intent(context, MainActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + .setAction(Intent.ACTION_MAIN) + + val pendingIntent = + PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT) + + val actions = if (showAction) { + listOf(buildAction()) + } else { + emptyList() + } + + return channel.buildNotification(pendingIntent, notificationText, actions) + } + + private fun buildAction(): NotificationCompat.Action { + val action = TunnelStateNotificationAction.from(tunnelState) + val label = context.getString(action.text) + + val intent = Intent(action.key).setPackage("net.mullvad.mullvadvpn") + val flags = PendingIntent.FLAG_UPDATE_CURRENT + + val pendingIntent = if (Build.VERSION.SDK_INT >= 26) { + PendingIntent.getForegroundService(context, 1, intent, flags) + } else { + PendingIntent.getService(context, 1, intent, flags) + } + + return NotificationCompat.Action(action.icon, label, pendingIntent) + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt new file mode 100644 index 0000000000..714264efbf --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt @@ -0,0 +1,54 @@ +package net.mullvad.mullvadvpn.service.notifications + +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.service.MullvadVpnService +import net.mullvad.talpid.tunnel.ActionAfterDisconnect + +enum class TunnelStateNotificationAction { + Connect, + Disconnect, + Cancel, + Dismiss; + + companion object { + fun from(tunnelState: TunnelState) = when (tunnelState) { + is TunnelState.Disconnected -> Connect + is TunnelState.Connecting -> Cancel + is TunnelState.Connected -> Disconnect + is TunnelState.Disconnecting -> { + when (tunnelState.actionAfterDisconnect) { + ActionAfterDisconnect.Reconnect -> Cancel + else -> Connect + } + } + is TunnelState.Error -> { + if (tunnelState.errorState.isBlocking) { + Disconnect + } else { + Dismiss + } + } + } + } + + val text + get() = when (this) { + Connect -> R.string.connect + Disconnect -> R.string.disconnect + Cancel -> R.string.cancel + Dismiss -> R.string.dismiss + } + + val key + get() = when (this) { + Connect -> MullvadVpnService.KEY_CONNECT_ACTION + else -> MullvadVpnService.KEY_DISCONNECT_ACTION + } + + val icon + get() = when (this) { + Connect -> R.drawable.icon_notification_connect + else -> R.drawable.icon_notification_disconnect + } +} diff --git a/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt b/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt index 395146b168..405722f010 100644 --- a/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt +++ b/android/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt @@ -13,7 +13,8 @@ import kotlin.properties.Delegates.observable class EventNotifier<T>(private val initialValue: T) { private val listeners = HashMap<Any, (T) -> Unit>() - private var latestEvent = initialValue + var latestEvent = initialValue + private set fun notify(event: T) { synchronized(this) { @@ -54,3 +55,16 @@ class EventNotifier<T>(private val initialValue: T) { notify(newValue) } } + +fun <T> autoSubscribable(id: Any, fallback: T, listener: (T) -> Unit) = + observable<EventNotifier<T>?>(null) { _, old, new -> + if (old != new) { + old?.unsubscribe(id) + + if (new == null) { + listener.invoke(fallback) + } else { + new.subscribe(id, listener) + } + } + } |
