Skip to content

Commit e05b019

Browse files
kasnderclaude
andcommitted
Warn user when another always-on VPN blocks onboarding
If another app is set as Always-on VPN, Android silently refuses to show TrackerControl's VPN consent dialog, so tapping "Enable On-Device VPN" in onboarding appears to do nothing. Detect this both proactively (pre-Android S, where the setting is readable) and reactively (via onActivityResult) and show a dialog pointing the user to system VPN settings so they can disable the other always-on VPN. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2c6ce76 commit e05b019

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

app/src/main/java/net/kollnig/missioncontrol/ActivityOnboarding.java

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import android.Manifest;
44
import android.content.Intent;
55
import android.content.SharedPreferences;
6+
import android.content.DialogInterface;
67
import android.net.VpnService;
78
import android.os.Build;
89
import android.os.Bundle;
910
import android.provider.Settings;
11+
import android.text.TextUtils;
1012
import android.util.Log;
1113
import android.view.LayoutInflater;
1214
import android.view.View;
@@ -23,6 +25,8 @@
2325
import androidx.recyclerview.widget.RecyclerView;
2426
import androidx.viewpager2.widget.ViewPager2;
2527

28+
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
29+
2630
import java.util.ArrayList;
2731
import java.util.List;
2832

@@ -274,9 +278,29 @@ private void refreshSlides() {
274278
slide.warningResId = vpnPrepared ? 0 : R.string.onboarding_vpn_sure;
275279
slide.actionListener = v -> {
276280
if (!vpnPrepared) {
277-
Intent intent = VpnService.prepare(ActivityOnboarding.this);
278-
if (intent != null) {
279-
startActivityForResult(intent, 0);
281+
// Proactively detect another Always-on VPN on Android < S,
282+
// where the setting is still readable.
283+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
284+
try {
285+
String alwaysOn = Settings.Secure.getString(
286+
getContentResolver(), "always_on_vpn_app");
287+
if (!TextUtils.isEmpty(alwaysOn)
288+
&& !getPackageName().equals(alwaysOn)) {
289+
showAlwaysOnVpnBlockedDialog();
290+
return;
291+
}
292+
} catch (Throwable ex) {
293+
Log.e("Onboarding", ex.toString());
294+
}
295+
}
296+
try {
297+
Intent intent = VpnService.prepare(ActivityOnboarding.this);
298+
if (intent != null) {
299+
startActivityForResult(intent, 0);
300+
}
301+
} catch (Throwable ex) {
302+
Log.e("Onboarding", ex.toString());
303+
showAlwaysOnVpnBlockedDialog();
280304
}
281305
}
282306
};
@@ -425,6 +449,36 @@ private void updateButtons(int position) {
425449
}
426450
}
427451

452+
@Override
453+
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
454+
super.onActivityResult(requestCode, resultCode, data);
455+
if (requestCode == 0 && resultCode != RESULT_OK) {
456+
// Consent was denied or silently blocked. If prepare() still returns a
457+
// non-null intent, Android refused to show the consent dialog — most
458+
// commonly because another app is set as Always-on VPN.
459+
if (VpnService.prepare(this) != null) {
460+
showAlwaysOnVpnBlockedDialog();
461+
}
462+
}
463+
}
464+
465+
private void showAlwaysOnVpnBlockedDialog() {
466+
new MaterialAlertDialogBuilder(this)
467+
.setTitle(R.string.onboarding_vpn_blocked_title)
468+
.setMessage(android.text.Html.fromHtml(
469+
getString(R.string.onboarding_vpn_blocked_desc)))
470+
.setPositiveButton(R.string.onboarding_vpn_blocked_action,
471+
(DialogInterface.OnClickListener) (dialog, which) -> {
472+
try {
473+
startActivity(new Intent(Settings.ACTION_VPN_SETTINGS));
474+
} catch (Throwable ex) {
475+
Log.e("Onboarding", ex.toString());
476+
}
477+
})
478+
.setNegativeButton(android.R.string.cancel, null)
479+
.show();
480+
}
481+
428482
private void finishOnboarding() {
429483
boolean vpnPrepared = VpnService.prepare(this) == null;
430484

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,9 @@ Sincerely,\n\n]]></string>
566566
<string name="onboarding_vpn_desc">TrackerControl uses Android\'s local VPN interface to analyse your internet traffic.\n\n<b>Important:</b> No external VPN server is involved. We don\'t see your data.</string>
567567
<string name="onboarding_vpn_action">Enable On-Device VPN</string>
568568
<string name="onboarding_vpn_sure">TrackerControl cannot filter any traffic without the local VPN. Are you sure you want to proceed without enabling it?</string>
569+
<string name="onboarding_vpn_blocked_title">Another VPN is active</string>
570+
<string name="onboarding_vpn_blocked_desc">Android prevented TrackerControl from starting its local VPN because another app is set as <b>Always-on VPN</b>. Open VPN settings and disable Always-on for the other VPN, then try again.</string>
571+
<string name="onboarding_vpn_blocked_action">Open VPN settings</string>
569572

570573
<string name="onboarding_battery_title">Prevent Interruption</string>
571574
<string name="onboarding_battery_desc">Android may kill background apps to save battery. To ensure continuous protection against trackers, please disable optimisations for TrackerControl.</string>

0 commit comments

Comments
 (0)