@@ -4,7 +4,10 @@ import android.content.Context
4
4
import android.content.Intent
5
5
import android.net.VpnService
6
6
import android.net.VpnService.prepare
7
+ import android.os.Build
7
8
import android.os.ParcelFileDescriptor
9
+ import android.provider.Settings
10
+ import androidx.annotation.DeprecatedSinceApi
8
11
import arrow.core.Either
9
12
import arrow.core.flatMap
10
13
import arrow.core.left
@@ -22,8 +25,9 @@ import net.mullvad.mullvadvpn.lib.model.Prepared
22
25
* Invoking VpnService.prepare() can result in 3 out comes:
23
26
* 1. IllegalStateException - There is a legacy VPN profile marked as always on
24
27
* 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)
27
31
* 3. null - The app has the VPN permission
28
32
*
29
33
* 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> =
44
48
if (intent == null ) {
45
49
Prepared .right()
46
50
} 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
+ }
52
56
}
57
+ return @flatMap PrepareError .NotPrepared (intent).left()
53
58
}
54
59
}
55
60
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
62
76
}
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 }
63
87
?.applicationInfo
64
88
?.loadLabel(packageManager)
65
89
?.toString()
0 commit comments