Skip to content

Commit b26d997

Browse files
committed
fix minimal mode and add tests
1 parent af94d21 commit b26d997

12 files changed

Lines changed: 309 additions & 49 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,18 @@ TrackerControl offers three **Blocking Modes**:
7878
| **Essential services** | Allowed | Allowed | Blocked |
7979
| **Ambiguous shared IPs** | Allowed | Allowed | Blocked |
8080
| **Granular per-tracker control** | No | Yes | Yes |
81-
| **Default for** | Play Store (Slim) | F-Droid / GitHub ||
81+
| **Auto-exclude known incompatible apps from VPN** | Yes | No | No |
82+
| **Current default for new installs** | Yes | No | No |
83+
84+
In practice, the modes differ as follows:
85+
86+
- **Minimal** uses only DuckDuckGo's mobile tracker list, blocks only trackers that DuckDuckGo marks as safe to block, does not offer per-tracker toggles, and automatically excludes known incompatible apps such as browsers from the VPN for compatibility.
87+
- **Standard** uses all tracker sources, keeps per-app and per-tracker controls enabled, and allows the `Content` category by default to reduce breakage.
88+
- **Strict** uses the same tracker sources as Standard, but also blocks the `Content` category and blocks ambiguous mixed shared-IP cases where a tracker hostname and a non-tracker hostname resolve to the same IP.
8289

8390
If you're interested in *blocking* tracking, then best download TrackerControl from [here](https://github.com/TrackerControl/tracker-control-android/releases/latest/download/TrackerControl-githubRelease-latest.apk), from [F-Droid](https://f-droid.org/packages/net.kollnig.missioncontrol.fdroid), or from the [IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/net.kollnig.missioncontrol) F-Droid Repository.
8491

85-
The version from [Google Play](https://play.google.com/store/apps/details?id=net.kollnig.missioncontrol.play) defaults to Minimal blocking mode (DuckDuckGo-compatible) for maximum app compatibility. All versions allow you to change the blocking mode in Settings.
92+
TrackerControl currently defaults to Minimal blocking mode for new installs. All versions allow you to change the blocking mode in Settings.
8693

8794
## Example Use
8895

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ android {
131131

132132
dependencies {
133133
implementation 'androidx.core:core-ktx:1.18.0'
134+
testImplementation 'junit:junit:4.13.2'
134135

135136
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
136137
implementation fileTree(dir: 'libs', include: ['*.jar'])

app/src/main/java/eu/faircode/netguard/ActivitySettings.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,11 @@ else if ("disable_on_call".equals(name)) {
515515
ServiceSinkhole.reload("changed " + name, this, false);
516516

517517
else if ("blocking_mode".equals(name)) {
518-
String mode = prefs.getString(name, BlockingMode.MODE_STANDARD);
518+
String mode = prefs.getString(name, BlockingMode.getDefaultMode());
519519
Preference pref = getPreferenceScreen().findPreference(name);
520520
if (pref != null)
521521
updateBlockingModeSummary(pref, mode);
522-
// Apply VPN exclusions for minimal mode
523-
if (BlockingMode.MODE_MINIMAL.equals(mode)) {
524-
BlockingMode.applyMinimalModeExclusions(this);
525-
}
522+
BlockingMode.syncModeExclusions(this);
526523
// Update Content category whitelist for all existing apps
527524
boolean isStrict = BlockingMode.MODE_STRICT.equals(mode);
528525
TrackerBlocklist b = TrackerBlocklist.getInstance(this);

app/src/main/java/eu/faircode/netguard/AdapterRule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ private void updateRule(Context context, Rule rule, boolean root, List<Rule> lis
515515
other.edit().putBoolean(rule.packageName, rule.other_blocked).apply();
516516

517517
apply.edit().putBoolean(rule.packageName, rule.apply).apply();
518+
BlockingMode.clearAutoExcludedApp(context, rule.packageName);
518519
tracker_protect.edit().putBoolean(rule.packageName, rule.tracker_protect).apply();
519520

520521
if (rule.screen_wifi == rule.screen_wifi_default)

app/src/main/java/eu/faircode/netguard/ApplicationEx.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ public void onCreate() {
148148
}
149149
}
150150

151-
// Apply minimal mode VPN exclusions on startup (for DDG-compatible blocking)
152-
BlockingMode.applyMinimalModeExclusions(this);
151+
// Keep VPN exclusions aligned with the selected blocking mode on startup.
152+
BlockingMode.syncModeExclusions(this);
153153

154154
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
155155
@Override
@@ -252,4 +252,4 @@ private void createNotificationChannels() {
252252
notify.setBypassDnd(true);
253253
nm.createNotificationChannel(access);
254254
}
255-
}
255+
}

app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import net.kollnig.missioncontrol.R;
8686
import net.kollnig.missioncontrol.analysis.TrackerAnalysisManager;
8787
import net.kollnig.missioncontrol.data.BlockingMode;
88+
import net.kollnig.missioncontrol.data.BlockingModeLogic;
8889
import net.kollnig.missioncontrol.data.InternetBlocklist;
8990
import net.kollnig.missioncontrol.data.Tracker;
9091
import net.kollnig.missioncontrol.data.TrackerBlocklist;
@@ -2232,7 +2233,8 @@ private boolean blockKnownTracker(String daddr, int uid) {
22322233
}
22332234

22342235
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
2235-
boolean blockAmbiguousTrackers = BlockingMode.isStrictMode(this);
2236+
String blockingMode = BlockingMode.getMode(this);
2237+
boolean blockAmbiguousTrackers = BlockingModeLogic.blocksAmbiguousTrackerIp(blockingMode);
22362238
Tracker tracker = null;
22372239
Expiring<Tracker> expiringTracker = ipToTracker.get(daddr);
22382240
if (expiringTracker != null) {
@@ -2340,23 +2342,16 @@ private boolean blockKnownTracker(String daddr, int uid) {
23402342
Log.i("TC-Log", app + " " + daddr + " " + ipToHost.get(daddr).getOrExpired() + " " + tracker.getName());
23412343
} else {
23422344
if (tracker != NO_TRACKER) {
2343-
if (BlockingMode.isMinimalMode(ServiceSinkhole.this)) {
2344-
// Minimal mode: block all non-Content DDG trackers, no granular control
2345-
return tracker != null
2346-
&& TrackerBlocklist.blockedTrackerMinimal(tracker);
2347-
} else if (BlockingMode.isStrictMode(ServiceSinkhole.this)) {
2348-
// Strict mode: block everything including Content category,
2349-
// but still respect per-app granular controls
2345+
boolean blockedByGranularRule = false;
2346+
if (!BlockingMode.MODE_MINIMAL.equals(blockingMode)) {
23502347
TrackerBlocklist b = TrackerBlocklist.getInstance(ServiceSinkhole.this);
2351-
return tracker != null
2352-
&& b.blockedTracker(uid, tracker);
2353-
} else {
2354-
// Standard mode: block trackers with granular control,
2355-
// Content category allowed by default
2356-
TrackerBlocklist b = TrackerBlocklist.getInstance(ServiceSinkhole.this);
2357-
return tracker != null
2358-
&& b.blockedTracker(uid, tracker);
2348+
blockedByGranularRule = b.blockedTracker(uid, tracker);
23592349
}
2350+
2351+
return BlockingModeLogic.shouldBlockKnownTracker(
2352+
blockingMode,
2353+
tracker.category,
2354+
blockedByGranularRule);
23602355
}
23612356
}
23622357

@@ -2630,6 +2625,7 @@ public void onReceive(Context context, Intent intent) {
26302625
context.getSharedPreferences("lockdown", Context.MODE_PRIVATE).edit().remove(packageName)
26312626
.apply();
26322627
context.getSharedPreferences("apply", Context.MODE_PRIVATE).edit().remove(packageName).apply();
2628+
BlockingMode.clearAutoExcludedApp(context, packageName);
26332629
context.getSharedPreferences("tracker_protect", Context.MODE_PRIVATE).edit().remove(packageName).apply();
26342630
context.getSharedPreferences("notify", Context.MODE_PRIVATE).edit().remove(packageName).apply();
26352631

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,9 +562,7 @@ else if (checkedId == R.id.rbResearch) {
562562
.putBoolean("block_dot", enableDotBlocking)
563563
.apply();
564564

565-
if (enableFilter && BlockingMode.MODE_MINIMAL.equals(mode)) {
566-
BlockingMode.applyMinimalModeExclusions(holder.itemView.getContext());
567-
}
565+
BlockingMode.syncModeExclusions(holder.itemView.getContext());
568566
});
569567
} else {
570568
SlideViewHolder holder = (SlideViewHolder) vh;

app/src/main/java/net/kollnig/missioncontrol/data/BlockingMode.java

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
import java.io.IOException;
3131
import java.io.InputStream;
3232
import java.nio.charset.StandardCharsets;
33+
import java.util.HashMap;
3334
import java.util.Collections;
3435
import java.util.HashSet;
36+
import java.util.Map;
3537
import java.util.Set;
3638

3739
import eu.faircode.netguard.Util;
@@ -49,9 +51,10 @@
4951
public class BlockingMode {
5052
private static final String TAG = BlockingMode.class.getSimpleName();
5153
public static final String PREF_BLOCKING_MODE = "blocking_mode";
52-
public static final String MODE_MINIMAL = "minimal";
53-
public static final String MODE_STANDARD = "standard";
54-
public static final String MODE_STRICT = "strict";
54+
private static final String PREF_MINIMAL_AUTO_EXCLUDED_APPS = "minimal_auto_excluded_apps";
55+
public static final String MODE_MINIMAL = BlockingModeLogic.MODE_MINIMAL;
56+
public static final String MODE_STANDARD = BlockingModeLogic.MODE_STANDARD;
57+
public static final String MODE_STRICT = BlockingModeLogic.MODE_STRICT;
5558

5659
private static Set<String> excludedApps;
5760

@@ -149,26 +152,59 @@ private static Set<String> loadExcludedApps(Context c) {
149152
}
150153

151154
/**
152-
* Apply minimal mode exclusions to the apply SharedPreferences.
153-
* Called when switching to minimal mode or on first run with minimal mode default.
155+
* Synchronize auto-managed VPN exclusions with the selected blocking mode.
156+
* Minimal mode auto-excludes known incompatible apps; standard/strict restore
157+
* any exclusions that were added automatically when minimal mode was active.
158+
*/
159+
public static void syncModeExclusions(Context c) {
160+
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
161+
SharedPreferences apply = c.getSharedPreferences("apply", Context.MODE_PRIVATE);
162+
BlockingModeLogic.ExclusionSyncResult result = BlockingModeLogic.syncVpnExclusions(
163+
getMode(c),
164+
getExcludedApps(c),
165+
getBooleanPrefs(apply),
166+
getAutoExcludedApps(prefs));
167+
168+
if (!result.applyFalsePackages.isEmpty() || !result.applyRemovals.isEmpty()) {
169+
SharedPreferences.Editor applyEditor = apply.edit();
170+
for (String packageName : result.applyRemovals)
171+
applyEditor.remove(packageName);
172+
for (String packageName : result.applyFalsePackages)
173+
applyEditor.putBoolean(packageName, false);
174+
applyEditor.apply();
175+
}
176+
177+
SharedPreferences.Editor prefsEditor = prefs.edit();
178+
if (result.autoExcludedApps.isEmpty())
179+
prefsEditor.remove(PREF_MINIMAL_AUTO_EXCLUDED_APPS);
180+
else
181+
prefsEditor.putStringSet(PREF_MINIMAL_AUTO_EXCLUDED_APPS, result.autoExcludedApps);
182+
prefsEditor.apply();
183+
184+
Log.i(TAG, (isMinimalMode(c) ? "Applied" : "Restored") + " mode-managed VPN exclusions");
185+
}
186+
187+
/**
188+
* Backwards-compatible entry point for callers that only knew about applying
189+
* minimal mode exclusions.
154190
*/
155191
public static void applyMinimalModeExclusions(Context c) {
156-
if (!isMinimalMode(c))
157-
return;
192+
syncModeExclusions(c);
193+
}
158194

159-
Set<String> excluded = getExcludedApps(c);
160-
SharedPreferences apply = c.getSharedPreferences("apply", Context.MODE_PRIVATE);
161-
SharedPreferences.Editor editor = apply.edit();
195+
public static void clearAutoExcludedApp(Context c, String packageName) {
196+
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
197+
Set<String> autoExcludedApps = getAutoExcludedApps(prefs);
198+
Set<String> updatedAutoExcludedApps = BlockingModeLogic.clearAutoExcludedApp(autoExcludedApps, packageName);
199+
if (autoExcludedApps.equals(updatedAutoExcludedApps))
200+
return;
162201

163-
for (String packageName : excluded) {
164-
// Only set to false if not already explicitly configured by user
165-
if (!apply.contains(packageName)) {
166-
editor.putBoolean(packageName, false);
167-
}
168-
}
202+
SharedPreferences.Editor editor = prefs.edit();
203+
if (updatedAutoExcludedApps.isEmpty())
204+
editor.remove(PREF_MINIMAL_AUTO_EXCLUDED_APPS);
205+
else
206+
editor.putStringSet(PREF_MINIMAL_AUTO_EXCLUDED_APPS, updatedAutoExcludedApps);
169207
editor.apply();
170-
171-
Log.i(TAG, "Applied minimal mode VPN exclusions");
172208
}
173209

174210
/**
@@ -177,4 +213,16 @@ public static void applyMinimalModeExclusions(Context c) {
177213
public static void invalidateCache() {
178214
excludedApps = null;
179215
}
216+
217+
private static Set<String> getAutoExcludedApps(SharedPreferences prefs) {
218+
return new HashSet<>(prefs.getStringSet(PREF_MINIMAL_AUTO_EXCLUDED_APPS, Collections.emptySet()));
219+
}
220+
221+
private static Map<String, Boolean> getBooleanPrefs(SharedPreferences prefs) {
222+
Map<String, Boolean> values = new HashMap<>();
223+
for (Map.Entry<String, ?> entry : prefs.getAll().entrySet())
224+
if (entry.getValue() instanceof Boolean)
225+
values.put(entry.getKey(), (Boolean) entry.getValue());
226+
return values;
227+
}
180228
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* TrackerControl is free software: you can redistribute it and/or modify
3+
* it under the terms of the GNU General Public License as published by
4+
* the Free Software Foundation, either version 3 of the License, or
5+
* (at your option) any later version.
6+
*
7+
* TrackerControl is distributed in the hope that it will be useful,
8+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
* GNU General Public License for more details.
11+
*
12+
* Copyright © 2026
13+
*/
14+
15+
package net.kollnig.missioncontrol.data;
16+
17+
import java.util.HashSet;
18+
import java.util.Map;
19+
import java.util.Set;
20+
21+
/**
22+
* Pure helpers for mode-specific blocking behavior and VPN exclusion syncing.
23+
*/
24+
public final class BlockingModeLogic {
25+
public static final String MODE_MINIMAL = "minimal";
26+
public static final String MODE_STANDARD = "standard";
27+
public static final String MODE_STRICT = "strict";
28+
public static final String CONTENT_CATEGORY = "Content";
29+
30+
private BlockingModeLogic() {
31+
}
32+
33+
public static boolean blocksAmbiguousTrackerIp(String mode) {
34+
return MODE_STRICT.equals(mode);
35+
}
36+
37+
public static boolean shouldBlockKnownTracker(String mode,
38+
String trackerCategory,
39+
boolean blockedByGranularRule) {
40+
if (MODE_MINIMAL.equals(mode))
41+
return !CONTENT_CATEGORY.equals(trackerCategory);
42+
43+
return blockedByGranularRule;
44+
}
45+
46+
public static ExclusionSyncResult syncVpnExclusions(String mode,
47+
Set<String> excludedApps,
48+
Map<String, Boolean> applyPrefs,
49+
Set<String> autoExcludedApps) {
50+
Set<String> nextAutoExcludedApps = new HashSet<>(autoExcludedApps);
51+
Set<String> applyFalsePackages = new HashSet<>();
52+
Set<String> applyRemovals = new HashSet<>();
53+
54+
if (MODE_MINIMAL.equals(mode)) {
55+
for (String packageName : new HashSet<>(nextAutoExcludedApps)) {
56+
if (!excludedApps.contains(packageName)) {
57+
if (Boolean.FALSE.equals(applyPrefs.get(packageName)))
58+
applyRemovals.add(packageName);
59+
nextAutoExcludedApps.remove(packageName);
60+
}
61+
}
62+
63+
for (String packageName : excludedApps) {
64+
if (!applyPrefs.containsKey(packageName)) {
65+
applyFalsePackages.add(packageName);
66+
nextAutoExcludedApps.add(packageName);
67+
} else if (Boolean.TRUE.equals(applyPrefs.get(packageName))) {
68+
nextAutoExcludedApps.remove(packageName);
69+
}
70+
}
71+
} else {
72+
for (String packageName : nextAutoExcludedApps)
73+
if (Boolean.FALSE.equals(applyPrefs.get(packageName)))
74+
applyRemovals.add(packageName);
75+
nextAutoExcludedApps.clear();
76+
}
77+
78+
return new ExclusionSyncResult(nextAutoExcludedApps, applyFalsePackages, applyRemovals);
79+
}
80+
81+
public static Set<String> clearAutoExcludedApp(Set<String> autoExcludedApps, String packageName) {
82+
Set<String> nextAutoExcludedApps = new HashSet<>(autoExcludedApps);
83+
nextAutoExcludedApps.remove(packageName);
84+
return nextAutoExcludedApps;
85+
}
86+
87+
public static final class ExclusionSyncResult {
88+
public final Set<String> autoExcludedApps;
89+
public final Set<String> applyFalsePackages;
90+
public final Set<String> applyRemovals;
91+
92+
ExclusionSyncResult(Set<String> autoExcludedApps,
93+
Set<String> applyFalsePackages,
94+
Set<String> applyRemovals) {
95+
this.autoExcludedApps = autoExcludedApps;
96+
this.applyFalsePackages = applyFalsePackages;
97+
this.applyRemovals = applyRemovals;
98+
}
99+
}
100+
}

app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ private void updateText(TextView tv, Tracker t) {
445445
if (!buttonView.isPressed())
446446
return;
447447
apply.edit().putBoolean(mAppId, !isChecked).apply();
448+
BlockingMode.clearAutoExcludedApp(mContext, mAppId);
448449

449450
AsyncTask.execute(() -> {
450451
Rule.clearCache(mContext);

0 commit comments

Comments
 (0)