diff options
| author | David Göransson <david.goransson@mullvad.net> | 2025-03-04 16:29:47 +0100 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2025-03-07 08:11:28 +0100 |
| commit | 80d86298cc7bfac65577a2b07a0ca27693945eb4 (patch) | |
| tree | 2e7dd8a899a74f28e6ac9cb8200418fd903847cc /android | |
| parent | 32fd95f7a81a3ab923838a29489af8336f3b6bf0 (diff) | |
| download | mullvadvpn-80d86298cc7bfac65577a2b07a0ca27693945eb4.tar.xz mullvadvpn-80d86298cc7bfac65577a2b07a0ca27693945eb4.zip | |
Add documentation about detecting always_on_vpn_app
Only before Android 11 and on test builds (running from Android studio)
it will report always-on vpn app.
Diffstat (limited to 'android')
4 files changed, 43 insertions, 30 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt index d505c44179..cd3e94bc65 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt @@ -160,6 +160,8 @@ class ConnectViewModel( if (hasVpnPermission) { connectionProxy.connect() } else { + // Either the user denied the permission or another always-on-vpn is active (if + // Android 11+ and run from Android Studio) _uiSideEffect.send(UiSideEffect.ConnectError.PermissionDenied) } } diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt index 992ae9404d..882279c999 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt @@ -3,11 +3,8 @@ package net.mullvad.mullvadvpn.lib.common.util import android.content.Context import android.content.Intent import android.net.Uri -import android.provider.Settings import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken -private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app" - fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): Uri { val urlString = buildString { append(accountUri) @@ -19,16 +16,6 @@ fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): U return Uri.parse(urlString) } -// NOTE: This function will return the current Always-on VPN package's name. In case of either -// Always-on VPN being disabled or not being able to read the state, NULL will be returned. -fun Context.resolveAlwaysOnVpnPackageName(): String? { - return try { - Settings.Secure.getString(contentResolver, ALWAYS_ON_VPN_APP) - } catch (ex: SecurityException) { - null - } -} - fun Context.openVpnSettings() { val intent = Intent("android.settings.VPN_SETTINGS") startActivity(intent) diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt index 06c862936b..dfc70609e1 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt @@ -4,7 +4,10 @@ import android.content.Context import android.content.Intent import android.net.VpnService import android.net.VpnService.prepare +import android.os.Build import android.os.ParcelFileDescriptor +import android.provider.Settings +import androidx.annotation.DeprecatedSinceApi import arrow.core.Either import arrow.core.flatMap import arrow.core.left @@ -22,8 +25,9 @@ import net.mullvad.mullvadvpn.lib.model.Prepared * Invoking VpnService.prepare() can result in 3 out comes: * 1. IllegalStateException - There is a legacy VPN profile marked as always on * 2. Intent - * - A: Can-prepare - Create Vpn profile - * - B: Always-on-VPN - Another Vpn Profile is marked as always on + * - A: Can-prepare - Create Vpn profile or Always-on-VPN is not detected in case of Android 11+ + * - B: Always-on-VPN - Another Vpn Profile is marked as always on (Only available up to Android + * 11 or where testOnly is set, e.g builds from Android Studio) * 3. null - The app has the VPN permission * * In case 1 and 2b, you don't know if you have a VPN profile or not. @@ -44,25 +48,44 @@ fun Context.prepareVpnSafe(): Either<PrepareError, Prepared> = if (intent == null) { Prepared.right() } else { - val alwaysOnVpnApp = getAlwaysOnVpnAppName() - if (alwaysOnVpnApp == null) { - PrepareError.NotPrepared(intent).left() - } else { - PrepareError.OtherAlwaysOnApp(alwaysOnVpnApp).left() + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + val alwaysOnVpnApp = getOtherAlwaysOnVpnAppName() + if (alwaysOnVpnApp != null) { + return@flatMap PrepareError.OtherAlwaysOnApp(alwaysOnVpnApp).left() + } } + return@flatMap PrepareError.NotPrepared(intent).left() } } -fun Context.getAlwaysOnVpnAppName(): String? { - return resolveAlwaysOnVpnPackageName() - ?.let { currentAlwaysOnVpn -> - packageManager.getInstalledPackagesList(0).singleOrNull { - it.packageName == currentAlwaysOnVpn && it.packageName != packageName - } +private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app" + +// NOTE: This function will return the current Always-on VPN package's name. In case of either +// Always-on VPN being disabled or not being able to read the state, null will be returned. +// +// Caveat: For Android 11+ it will always return null unless the app is a test build (e.g running +// from Android Studio). +@DeprecatedSinceApi(Build.VERSION_CODES.S) +fun Context.getOtherAlwaysOnVpnAppName(): String? { + val currentAlwaysOnPackageName = + try { + Settings.Secure.getString(contentResolver, ALWAYS_ON_VPN_APP) + } catch (ex: SecurityException) { + return null } - ?.applicationInfo - ?.loadLabel(packageManager) - ?.toString() + + // If we are the current Always-on VPN app, we return null + return if (currentAlwaysOnPackageName == packageName) { + null + } else { + // Resolve package name to app name + packageManager + .getInstalledPackagesList(0) + .firstOrNull { it.packageName == currentAlwaysOnPackageName } + ?.applicationInfo + ?.loadLabel(packageManager) + ?.toString() + } } /** diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt index 2fea9a9211..f26a480417 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt @@ -8,9 +8,10 @@ sealed interface PrepareError : PrepareResult { // Legacy VPN profile is active as Always-on data object OtherLegacyAlwaysOnVpn : PrepareError - // Another VPN app is active as Always-on + // Another VPN app is active as Always-on (Only works up to Android 11 or debug builds) data class OtherAlwaysOnApp(val appName: String) : PrepareError + // VPN profile can be created or Always-on VPN is active but not detected data class NotPrepared(val prepareIntent: Intent) : PrepareError } |
