From 3d000007788cd63a05496878b1eadee011043e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Thu, 23 Nov 2023 15:18:43 +0100 Subject: [PATCH] Fix crash with auto-connect and no permission --- .../service/ForegroundNotificationManager.kt | 24 +- .../mullvadvpn/tile/MullvadTileService.kt | 231 ++++++++++-------- 2 files changed, 150 insertions(+), 105 deletions(-) diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt index b360613eaaed..dad6ea5b5681 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt @@ -1,6 +1,9 @@ package net.mullvad.mullvadvpn.service import android.app.Service +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED +import android.net.VpnService +import android.os.Build import kotlin.properties.Delegates.observable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -90,11 +93,22 @@ class ForegroundNotificationManager( } fun showOnForeground() { - service.startForeground( - TunnelStateNotification.NOTIFICATION_ID, - tunnelStateNotification.build() - ) - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (VpnService.prepare(service) == null) { + service.startForeground( + TunnelStateNotification.NOTIFICATION_ID, + tunnelStateNotification.build(), + FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED + ) + } else { + return + } + } else { + service.startForeground( + TunnelStateNotification.NOTIFICATION_ID, + tunnelStateNotification.build(), + ) + } onForeground = true } diff --git a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt index 2a51c4e0e7b5..31747ecda5e1 100644 --- a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt +++ b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt @@ -1,7 +1,11 @@ package net.mullvad.mullvadvpn.tile +import android.annotation.SuppressLint +import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Icon +import android.net.VpnService +import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import android.util.Log @@ -17,129 +21,156 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeoutOrNull import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION import net.mullvad.mullvadvpn.lib.common.constant.KEY_DISCONNECT_ACTION +import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS +import net.mullvad.mullvadvpn.lib.common.util.SdkUtils import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.setSubtitleIfSupported import net.mullvad.mullvadvpn.model.ServiceResult import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.talpid.tunnel.ActionAfterDisconnect class MullvadTileService : TileService() { - private var scope: CoroutineScope? = null + private var scope: CoroutineScope? = null - private lateinit var securedIcon: Icon - private lateinit var unsecuredIcon: Icon + private lateinit var securedIcon: Icon + private lateinit var unsecuredIcon: Icon - override fun onCreate() { - securedIcon = Icon.createWithResource(this, R.drawable.small_logo_white) - unsecuredIcon = Icon.createWithResource(this, R.drawable.small_logo_black) - } + override fun onCreate() { + securedIcon = Icon.createWithResource(this, R.drawable.small_logo_white) + unsecuredIcon = Icon.createWithResource(this, R.drawable.small_logo_black) + } - override fun onClick() { - // Workaround for the reported bug: https://issuetracker.google.com/issues/236862865 - suspend fun isUnlockStatusPropagatedWithinTimeout( - unlockTimeoutMillis: Long, - unlockCheckDelayMillis: Long - ): Boolean { - return withTimeoutOrNull(unlockTimeoutMillis) { - while (isLocked) { - delay(unlockCheckDelayMillis) - } - return@withTimeoutOrNull true - } - ?: false + override fun onClick() { + // Workaround for the reported bug: https://issuetracker.google.com/issues/236862865 + suspend fun isUnlockStatusPropagatedWithinTimeout( + unlockTimeoutMillis: Long, + unlockCheckDelayMillis: Long + ): Boolean { + return withTimeoutOrNull(unlockTimeoutMillis) { + while (isLocked) { + delay(unlockCheckDelayMillis) } + return@withTimeoutOrNull true + } ?: false + } - unlockAndRun { - runBlocking { - val isUnlockStatusPropagated = - isUnlockStatusPropagatedWithinTimeout( - unlockTimeoutMillis = 1000L, - unlockCheckDelayMillis = 100L - ) - - if (isUnlockStatusPropagated) { - toggleTunnel() - } else { - Log.e("mullvad", "Unable to toggle tunnel state") - } - } + unlockAndRun { + runBlocking { + val isUnlockStatusPropagated = + isUnlockStatusPropagatedWithinTimeout( + unlockTimeoutMillis = 1000L, unlockCheckDelayMillis = 100L) + + if (isUnlockStatusPropagated) { + toggleTunnel() + } else { + Log.e("mullvad", "Unable to toggle tunnel state") } + } } + } - override fun onStartListening() { - scope = MainScope().apply { launchListenToTunnelState() } - } + override fun onStartListening() { + scope = MainScope().apply { launchListenToTunnelState() } + } - override fun onStopListening() { - scope?.cancel() - } + override fun onStopListening() { + scope?.cancel() + } - private fun toggleTunnel() { - val intent = - Intent().apply { - setClassName(applicationContext.packageName, VPN_SERVICE_CLASS) - action = - if (qsTile.state == Tile.STATE_INACTIVE) { - KEY_CONNECT_ACTION - } else { - KEY_DISCONNECT_ACTION - } - } - - // Always start as foreground in case tile is out-of-sync. - startForegroundService(intent) - } + @SuppressLint("StartActivityAndCollapseDeprecated") + private fun toggleTunnel() { + val isSetup = VpnService.prepare(applicationContext) == null + // TODO This logic should be more advanced, we should ensure user has an account setup etc. + if (!isSetup) { + Log.d("MullvadTileService", "VPN service not setup, starting main activity") + + val intent = + Intent().apply { + setClassName(applicationContext.packageName, MAIN_ACTIVITY_CLASS) + flags = + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP or + Intent.FLAG_ACTIVITY_NEW_TASK + action = Intent.ACTION_MAIN + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val pendingIntent = + PendingIntent.getActivity( + applicationContext, 1, intent, SdkUtils.getSupportedPendingIntentFlags()) + startActivityAndCollapse(pendingIntent) + } else { + startActivityAndCollapse(intent) + } - @OptIn(FlowPreview::class) - private fun CoroutineScope.launchListenToTunnelState() = launch { - ServiceConnection(this@MullvadTileService, this) - .tunnelState - .debounce(300L) - .map { (tunnelState, connectionState) -> mapToTileState(tunnelState, connectionState) } - .collect { updateTileState(it) } + return + } else { + Log.d("MullvadTileService", "VPN service is setup") } + val intent = + Intent().apply { + setClassName(applicationContext.packageName, VPN_SERVICE_CLASS) + action = + if (qsTile.state == Tile.STATE_INACTIVE) { + KEY_CONNECT_ACTION + } else { + KEY_DISCONNECT_ACTION + } + } - private fun mapToTileState( - tunnelState: TunnelState, - connectionState: ServiceResult.ConnectionState - ): Int { - return if (connectionState == ServiceResult.ConnectionState.CONNECTED) { - when (tunnelState) { - is TunnelState.Disconnected -> Tile.STATE_INACTIVE - is TunnelState.Connecting -> Tile.STATE_ACTIVE - is TunnelState.Connected -> Tile.STATE_ACTIVE - is TunnelState.Disconnecting -> { - if (tunnelState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) { - Tile.STATE_ACTIVE - } else { - Tile.STATE_INACTIVE - } - } - is TunnelState.Error -> { - if (tunnelState.errorState.isBlocking) { - Tile.STATE_ACTIVE - } else { - Tile.STATE_INACTIVE - } - } - } - } else { + // Always start as foreground in case tile is out-of-sync. + startForegroundService(intent) + } + + @OptIn(FlowPreview::class) + private fun CoroutineScope.launchListenToTunnelState() = launch { + ServiceConnection(this@MullvadTileService, this) + .tunnelState + .debounce(300L) + .map { (tunnelState, connectionState) -> mapToTileState(tunnelState, connectionState) } + .collect { updateTileState(it) } + } + + private fun mapToTileState( + tunnelState: TunnelState, + connectionState: ServiceResult.ConnectionState + ): Int { + return if (connectionState == ServiceResult.ConnectionState.CONNECTED) { + when (tunnelState) { + is TunnelState.Disconnected -> Tile.STATE_INACTIVE + is TunnelState.Connecting -> Tile.STATE_ACTIVE + is TunnelState.Connected -> Tile.STATE_ACTIVE + is TunnelState.Disconnecting -> { + if (tunnelState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) { + Tile.STATE_ACTIVE + } else { Tile.STATE_INACTIVE + } } + is TunnelState.Error -> { + if (tunnelState.errorState.isBlocking) { + Tile.STATE_ACTIVE + } else { + Tile.STATE_INACTIVE + } + } + } + } else { + Tile.STATE_INACTIVE } + } - private fun updateTileState(newState: Int) { - qsTile?.apply { - if (newState == Tile.STATE_ACTIVE) { - state = Tile.STATE_ACTIVE - icon = securedIcon - setSubtitleIfSupported(resources.getText(R.string.secured)) - } else { - state = Tile.STATE_INACTIVE - icon = unsecuredIcon - setSubtitleIfSupported(resources.getText(R.string.unsecured)) - } - updateTile() - } + private fun updateTileState(newState: Int) { + qsTile?.apply { + if (newState == Tile.STATE_ACTIVE) { + state = Tile.STATE_ACTIVE + icon = securedIcon + setSubtitleIfSupported(resources.getText(R.string.secured)) + } else { + state = Tile.STATE_INACTIVE + icon = unsecuredIcon + setSubtitleIfSupported(resources.getText(R.string.unsecured)) + } + updateTile() } + } }