Skip to content

Commit 58566fe

Browse files
committed
add tests, fix critical bugs
1 parent b26d997 commit 58566fe

14 files changed

Lines changed: 534 additions & 59 deletions

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -519,17 +519,7 @@ else if ("blocking_mode".equals(name)) {
519519
Preference pref = getPreferenceScreen().findPreference(name);
520520
if (pref != null)
521521
updateBlockingModeSummary(pref, mode);
522-
BlockingMode.syncModeExclusions(this);
523-
// Update Content category whitelist for all existing apps
524-
boolean isStrict = BlockingMode.MODE_STRICT.equals(mode);
525-
TrackerBlocklist b = TrackerBlocklist.getInstance(this);
526-
if (b.applyStrictModeToAll(isStrict))
527-
b.saveSettings(this);
528-
// Clear cached tracker lookups (ambiguous IP decisions depend on mode)
529-
ServiceSinkhole.clearTrackerCaches();
530-
// Reload tracker data and VPN rules
531-
TrackerList.reloadTrackerData(this);
532-
ServiceSinkhole.reload("changed " + name, this, false);
522+
BlockingMode.applyMode(this);
533523
}
534524

535525
else if ("log_logcat".equals(name)) {

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

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import android.app.NotificationChannel;
3030
import android.app.NotificationManager;
3131
import android.content.Context;
32+
import android.content.SharedPreferences;
3233
import android.graphics.Color;
3334
import android.graphics.drawable.ColorDrawable;
3435
import android.os.Build;
@@ -119,34 +120,8 @@ public void onCreate() {
119120
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
120121
createNotificationChannels();
121122

122-
// Migrate onboarding_complete boolean to onboarding_version int
123-
android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
124-
if (prefs.contains("onboarding_complete") && !prefs.contains("onboarding_version")) {
125-
boolean completed = prefs.getBoolean("onboarding_complete", false);
126-
prefs.edit()
127-
.remove("onboarding_complete")
128-
.putInt("onboarding_version", completed ? 1 : 0)
129-
.apply();
130-
Log.i(TAG, "Migrated onboarding_complete=" + completed + " -> onboarding_version=" + (completed ? 1 : 0));
131-
}
132-
133-
// Migrate old strict_blocking pref to blocking_mode
134-
if (!prefs.contains(BlockingMode.PREF_BLOCKING_MODE)) {
135-
String migratedMode = BlockingMode.getDefaultMode();
136-
137-
if (prefs.contains("strict_blocking")) {
138-
boolean oldStrict = prefs.getBoolean("strict_blocking", false);
139-
migratedMode = oldStrict ? BlockingMode.MODE_STRICT : BlockingMode.MODE_STANDARD;
140-
141-
prefs.edit()
142-
.remove("strict_blocking")
143-
.putString(BlockingMode.PREF_BLOCKING_MODE, migratedMode)
144-
.apply();
145-
Log.i(TAG, "Migrated strict_blocking=" + oldStrict + " -> mode=" + migratedMode);
146-
} else {
147-
prefs.edit().putString(BlockingMode.PREF_BLOCKING_MODE, migratedMode).apply();
148-
}
149-
}
123+
SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
124+
migratePreferences(prefs);
150125

151126
// Keep VPN exclusions aligned with the selected blocking mode on startup.
152127
BlockingMode.syncModeExclusions(this);
@@ -231,6 +206,51 @@ public void onActivityDestroyed(@NonNull Activity activity) {
231206
});
232207
}
233208

209+
static void migratePreferences(SharedPreferences prefs) {
210+
if (prefs.contains("onboarding_complete") && !prefs.contains("onboarding_version")) {
211+
boolean completed = prefs.getBoolean("onboarding_complete", false);
212+
prefs.edit()
213+
.remove("onboarding_complete")
214+
.putInt("onboarding_version", completed ? 1 : 0)
215+
.apply();
216+
Log.i(TAG, "Migrated onboarding_complete=" + completed + " -> onboarding_version=" + (completed ? 1 : 0));
217+
}
218+
219+
if (!prefs.contains(BlockingMode.PREF_BLOCKING_MODE)) {
220+
Boolean oldStrict = prefs.contains("strict_blocking")
221+
? prefs.getBoolean("strict_blocking", false)
222+
: null;
223+
int installedVersion = prefs.getInt("version", -1);
224+
String migratedMode = resolveBlockingModeMigration(null, oldStrict, installedVersion);
225+
226+
if (oldStrict != null) {
227+
prefs.edit()
228+
.remove("strict_blocking")
229+
.putString(BlockingMode.PREF_BLOCKING_MODE, migratedMode)
230+
.apply();
231+
Log.i(TAG, "Migrated strict_blocking=" + oldStrict + " -> mode=" + migratedMode);
232+
} else {
233+
prefs.edit().putString(BlockingMode.PREF_BLOCKING_MODE, migratedMode).apply();
234+
}
235+
}
236+
}
237+
238+
static String resolveBlockingModeMigration(@Nullable String existingMode, @Nullable Boolean legacyStrict) {
239+
return resolveBlockingModeMigration(existingMode, legacyStrict, -1);
240+
}
241+
242+
static String resolveBlockingModeMigration(@Nullable String existingMode,
243+
@Nullable Boolean legacyStrict,
244+
int installedVersion) {
245+
if (existingMode != null)
246+
return existingMode;
247+
if (legacyStrict != null)
248+
return legacyStrict ? BlockingMode.MODE_STRICT : BlockingMode.MODE_STANDARD;
249+
if (installedVersion >= 0)
250+
return BlockingMode.MODE_STANDARD;
251+
return BlockingMode.getDefaultMode();
252+
}
253+
234254
@TargetApi(Build.VERSION_CODES.O)
235255
private void createNotificationChannels() {
236256
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2117,7 +2117,7 @@ private boolean isSupported(int protocol) {
21172117
static String NO_DNAME = "null"; // use a String, unequal the real null
21182118
static Tracker NO_TRACKER = new Tracker(null, null, 0);
21192119

2120-
static void clearTrackerCaches() {
2120+
public static void clearTrackerCaches() {
21212121
ipToHost.clear();
21222122
ipToTracker.clear();
21232123
}

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

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

565-
BlockingMode.syncModeExclusions(holder.itemView.getContext());
565+
BlockingMode.applyMode(holder.itemView.getContext());
566566
});
567567
} else {
568568
SlideViewHolder holder = (SlideViewHolder) vh;

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

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@
2323

2424
import androidx.preference.PreferenceManager;
2525

26-
import org.json.JSONArray;
27-
import org.json.JSONException;
28-
import org.json.JSONObject;
29-
3026
import java.io.IOException;
3127
import java.io.InputStream;
3228
import java.nio.charset.StandardCharsets;
@@ -36,6 +32,7 @@
3632
import java.util.Map;
3733
import java.util.Set;
3834

35+
import eu.faircode.netguard.ServiceSinkhole;
3936
import eu.faircode.netguard.Util;
4037

4138
/**
@@ -87,7 +84,7 @@ public static boolean isStrictMode(Context c) {
8784
public static boolean isTrackerProtectionEnabled(Context c,
8885
SharedPreferences trackerProtectPrefs,
8986
String packageName) {
90-
return !isMinimalMode(c) || trackerProtectPrefs.getBoolean(packageName, true);
87+
return isMinimalMode(c) || trackerProtectPrefs.getBoolean(packageName, true);
9188
}
9289

9390
/**
@@ -131,21 +128,10 @@ private static Set<String> loadExcludedApps(Context c) {
131128
throw new IOException("No bytes read.");
132129

133130
String json = new String(buffer, StandardCharsets.UTF_8);
134-
JSONObject root = new JSONObject(json);
135-
136-
// Load all categories
137-
String[] categories = {"browsers", "system_ims", "vpn_incompatible", "user_reported"};
138-
for (String category : categories) {
139-
if (root.has(category)) {
140-
JSONArray arr = root.getJSONArray(category);
141-
for (int i = 0; i < arr.length(); i++) {
142-
apps.add(arr.getString(i));
143-
}
144-
}
145-
}
131+
apps.addAll(BlockingModeLogic.parseExcludedAppsJson(json));
146132

147133
Log.i(TAG, "Loaded " + apps.size() + " excluded apps for minimal mode");
148-
} catch (IOException | JSONException e) {
134+
} catch (IOException e) {
149135
Log.e(TAG, "Failed to load excluded apps", e);
150136
}
151137
return Collections.unmodifiableSet(apps);
@@ -184,6 +170,21 @@ public static void syncModeExclusions(Context c) {
184170
Log.i(TAG, (isMinimalMode(c) ? "Applied" : "Restored") + " mode-managed VPN exclusions");
185171
}
186172

173+
/**
174+
* Apply all runtime side effects of the current blocking mode.
175+
*/
176+
public static void applyMode(Context c) {
177+
syncModeExclusions(c);
178+
179+
TrackerBlocklist trackerBlocklist = TrackerBlocklist.getInstance(c);
180+
if (trackerBlocklist.applyStrictModeToAll(isStrictMode(c)))
181+
trackerBlocklist.saveSettings(c);
182+
183+
ServiceSinkhole.clearTrackerCaches();
184+
TrackerList.reloadTrackerData(c);
185+
ServiceSinkhole.reload("changed " + PREF_BLOCKING_MODE, c, false);
186+
}
187+
187188
/**
188189
* Backwards-compatible entry point for callers that only knew about applying
189190
* minimal mode exclusions.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import java.util.HashSet;
1818
import java.util.Map;
1919
import java.util.Set;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
2022

2123
/**
2224
* Pure helpers for mode-specific blocking behavior and VPN exclusion syncing.
@@ -84,6 +86,24 @@ public static Set<String> clearAutoExcludedApp(Set<String> autoExcludedApps, Str
8486
return nextAutoExcludedApps;
8587
}
8688

89+
static Set<String> parseExcludedAppsJson(String json) {
90+
Set<String> apps = new HashSet<>();
91+
for (String category : new String[] { "browsers", "system_ims", "vpn_incompatible", "user_reported" }) {
92+
Matcher categoryMatcher = Pattern.compile(
93+
"\"" + Pattern.quote(category) + "\"\\s*:\\s*\\[(.*?)]",
94+
Pattern.DOTALL)
95+
.matcher(json);
96+
if (!categoryMatcher.find())
97+
continue;
98+
99+
Matcher valueMatcher = Pattern.compile("\"([^\"]+)\"").matcher(categoryMatcher.group(1));
100+
while (valueMatcher.find())
101+
apps.add(valueMatcher.group(1));
102+
}
103+
104+
return apps;
105+
}
106+
87107
public static final class ExclusionSyncResult {
88108
public final Set<String> autoExcludedApps;
89109
public final Set<String> applyFalsePackages;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ public static TrackerBlocklist getInstance(Context c) {
6161
return instance;
6262
}
6363

64+
static synchronized void resetForTests() {
65+
instance = null;
66+
}
67+
6468
/**
6569
* For a given tracker company, this computes a key to store the blocking state
6670
* of this tracker.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,4 +550,5 @@ private void addTrackerDomain(Tracker tracker, String dom) {
550550
} else
551551
hostnameToTracker.put(dom, tracker);
552552
}
553+
553554
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* This file is from NetGuard.
3+
*
4+
* NetGuard is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* NetGuard is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* Copyright © 2026
15+
*/
16+
17+
package eu.faircode.netguard;
18+
19+
import static org.junit.Assert.assertEquals;
20+
21+
import net.kollnig.missioncontrol.data.BlockingMode;
22+
23+
import org.junit.Test;
24+
25+
public class BlockingModeMigrationTest {
26+
27+
@Test
28+
public void legacyStrictBlockingTrueMigratesToStrictMode() {
29+
assertEquals(BlockingMode.MODE_STRICT,
30+
ApplicationEx.resolveBlockingModeMigration(null, true));
31+
}
32+
33+
@Test
34+
public void legacyStrictBlockingFalseMigratesToStandardMode() {
35+
assertEquals(BlockingMode.MODE_STANDARD,
36+
ApplicationEx.resolveBlockingModeMigration(null, false));
37+
}
38+
39+
@Test
40+
public void freshInstallDefaultsToMinimalMode() {
41+
assertEquals(BlockingMode.MODE_MINIMAL,
42+
ApplicationEx.resolveBlockingModeMigration(null, null));
43+
}
44+
45+
@Test
46+
public void existingBlockingModeWinsOverLegacyPreference() {
47+
assertEquals(BlockingMode.MODE_STANDARD,
48+
ApplicationEx.resolveBlockingModeMigration(BlockingMode.MODE_STANDARD, true));
49+
}
50+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 static org.junit.Assert.assertEquals;
18+
import static org.junit.Assert.assertFalse;
19+
import static org.junit.Assert.assertTrue;
20+
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
24+
import java.io.IOException;
25+
import java.lang.reflect.Field;
26+
import java.nio.charset.StandardCharsets;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
30+
public class BlockingBaselineSubsetTest {
31+
32+
@Before
33+
public void setUp() throws Exception {
34+
Field instance = TrackerBlocklist.class.getDeclaredField("instance");
35+
instance.setAccessible(true);
36+
instance.set(null, null);
37+
}
38+
39+
@Test
40+
public void bundledXrayAndDisconnectListsContainRepresentativeDomains() throws IOException {
41+
String xray = new String(Files.readAllBytes(assetPath("xray-blacklist.json")), StandardCharsets.UTF_8);
42+
String disconnect = new StringBuilder(
43+
new String(Files.readAllBytes(assetPath("disconnect-blacklist.reversed.json")), StandardCharsets.UTF_8))
44+
.reverse()
45+
.toString();
46+
47+
for (String domain : new String[] {
48+
"doubleclick.net",
49+
"google-analytics.com",
50+
"crashlytics.com",
51+
"branch.io"
52+
}) {
53+
assertTrue(xray.contains(domain));
54+
assertTrue(disconnect.contains(domain));
55+
}
56+
}
57+
58+
@Test
59+
public void sharedTrackerBlocklistSemanticsRemainStable() {
60+
TrackerBlocklist blocklist = TrackerBlocklist.getInstance(null);
61+
Tracker tracker = new Tracker("Branch", "Advertising");
62+
63+
assertTrue(blocklist.blockedTracker(1001, tracker));
64+
65+
blocklist.unblock(1001, tracker.category);
66+
assertFalse(blocklist.blockedTracker(1001, tracker));
67+
68+
blocklist.block(1001, tracker.category);
69+
blocklist.unblock(1001, tracker);
70+
assertFalse(blocklist.blockedTracker(1001, tracker));
71+
}
72+
73+
@Test
74+
public void blockingKeyNormalizationMatchesLegacyTrackerMigrations() {
75+
assertEquals("Uncategorised | Google",
76+
TrackerBlocklist.getBlockingKey(new Tracker("Alphabet", "Uncategorised")));
77+
}
78+
79+
private static Path assetPath(String assetName) {
80+
Path moduleRelative = Path.of("src", "main", "assets", assetName);
81+
if (Files.exists(moduleRelative))
82+
return moduleRelative;
83+
84+
return Path.of("app", "src", "main", "assets", assetName);
85+
}
86+
}

0 commit comments

Comments
 (0)