Skip to content

Commit 882c61f

Browse files
committed
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.
1 parent 32fd95f commit 882c61f

File tree

4 files changed

+41
-25
lines changed

4 files changed

+41
-25
lines changed

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt

+2
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ class ConnectViewModel(
160160
if (hasVpnPermission) {
161161
connectionProxy.connect()
162162
} else {
163+
// Either the user denied the permission or another always-on-vpn is active (if
164+
// Android 11+ and run from Android Studio)
163165
_uiSideEffect.send(UiSideEffect.ConnectError.PermissionDenied)
164166
}
165167
}

android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt

-11
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package net.mullvad.mullvadvpn.lib.common.util
33
import android.content.Context
44
import android.content.Intent
55
import android.net.Uri
6-
import android.provider.Settings
76
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
87

9-
private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app"
108

119
fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): Uri {
1210
val urlString = buildString {
@@ -19,15 +17,6 @@ fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): U
1917
return Uri.parse(urlString)
2018
}
2119

22-
// NOTE: This function will return the current Always-on VPN package's name. In case of either
23-
// Always-on VPN being disabled or not being able to read the state, NULL will be returned.
24-
fun Context.resolveAlwaysOnVpnPackageName(): String? {
25-
return try {
26-
Settings.Secure.getString(contentResolver, ALWAYS_ON_VPN_APP)
27-
} catch (ex: SecurityException) {
28-
null
29-
}
30-
}
3120

3221
fun Context.openVpnSettings() {
3322
val intent = Intent("android.settings.VPN_SETTINGS")

android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt

+37-13
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import android.content.Context
44
import android.content.Intent
55
import android.net.VpnService
66
import android.net.VpnService.prepare
7+
import android.os.Build
78
import android.os.ParcelFileDescriptor
9+
import android.provider.Settings
10+
import androidx.annotation.DeprecatedSinceApi
811
import arrow.core.Either
912
import arrow.core.flatMap
1013
import arrow.core.left
@@ -22,8 +25,9 @@ import net.mullvad.mullvadvpn.lib.model.Prepared
2225
* Invoking VpnService.prepare() can result in 3 out comes:
2326
* 1. IllegalStateException - There is a legacy VPN profile marked as always on
2427
* 2. Intent
25-
* - A: Can-prepare - Create Vpn profile
26-
* - B: Always-on-VPN - Another Vpn Profile is marked as always on
28+
* - A: Can-prepare - Create Vpn profile or Always-on-VPN is not detected in case of Android 11+
29+
* - B: Always-on-VPN - Another Vpn Profile is marked as always on (Only available up to Android
30+
* 11 or where testOnly is set, e.g builds from Android Studio)
2731
* 3. null - The app has the VPN permission
2832
*
2933
* In case 1 and 2b, you don't know if you have a VPN profile or not.
@@ -44,22 +48,42 @@ fun Context.prepareVpnSafe(): Either<PrepareError, Prepared> =
4448
if (intent == null) {
4549
Prepared.right()
4650
} else {
47-
val alwaysOnVpnApp = getAlwaysOnVpnAppName()
48-
if (alwaysOnVpnApp == null) {
49-
PrepareError.NotPrepared(intent).left()
50-
} else {
51-
PrepareError.OtherAlwaysOnApp(alwaysOnVpnApp).left()
51+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
52+
val alwaysOnVpnApp = getOtherAlwaysOnVpnAppName()
53+
if (alwaysOnVpnApp != null) {
54+
return@flatMap PrepareError.OtherAlwaysOnApp(alwaysOnVpnApp).left()
55+
}
5256
}
57+
return@flatMap PrepareError.NotPrepared(intent).left()
5358
}
5459
}
5560

56-
fun Context.getAlwaysOnVpnAppName(): String? {
57-
return resolveAlwaysOnVpnPackageName()
58-
?.let { currentAlwaysOnVpn ->
59-
packageManager.getInstalledPackagesList(0).singleOrNull {
60-
it.packageName == currentAlwaysOnVpn && it.packageName != packageName
61-
}
61+
private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app"
62+
63+
// NOTE: This function will return the current Always-on VPN package's name. In case of either
64+
// Always-on VPN being disabled or not being able to read the state, null will be returned.
65+
//
66+
// Caveat: For Android 11+ it will always return null unless the app is a test build (e.g running
67+
// from Android Studio).
68+
@DeprecatedSinceApi(Build.VERSION_CODES.S)
69+
@Suppress("ReturnCount")
70+
fun Context.getOtherAlwaysOnVpnAppName(): String? {
71+
val currentAlwaysOnPackageName =
72+
try {
73+
Settings.Secure.getString(contentResolver, ALWAYS_ON_VPN_APP)
74+
} catch (ex: SecurityException) {
75+
return null
6276
}
77+
78+
// If we are the current Always-on VPN app, we return null
79+
if (currentAlwaysOnPackageName == packageName) {
80+
return null
81+
}
82+
83+
// Resolve package name to app name
84+
return packageManager
85+
.getInstalledPackagesList(0)
86+
.firstOrNull { it.packageName == currentAlwaysOnPackageName }
6387
?.applicationInfo
6488
?.loadLabel(packageManager)
6589
?.toString()

android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ sealed interface PrepareError : PrepareResult {
88
// Legacy VPN profile is active as Always-on
99
data object OtherLegacyAlwaysOnVpn : PrepareError
1010

11-
// Another VPN app is active as Always-on
11+
// Another VPN app is active as Always-on (Only works up to Android 11 or debug builds)
1212
data class OtherAlwaysOnApp(val appName: String) : PrepareError
1313

14+
// VPN profile can be created or Always-on VPN is active but not detected
1415
data class NotPrepared(val prepareIntent: Intent) : PrepareError
1516
}
1617

0 commit comments

Comments
 (0)