From 58a9e0abc93df4f830e0c262b21b16560366c17c Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:56:45 +0200 Subject: [PATCH 1/2] Make filtered-but-hidden apps reachable via search (#420, #483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some installed apps are routed through the tun and filtered/blocked by TC (blocking is keyed on UID in ServiceSinkhole.isAddressAllowed / blockKnownTracker), yet never appear in the app list, so the user cannot open them to add an exception or exclude them. The only workaround is disabling TC entirely. Root cause: Rule.getRules(false, ...) applies the show_system / show_frozen / show_nointernet display filters when building the visible list, but the packet path filters by UID regardless of those display filters. An app hidden by a display filter (e.g. an OEM-preinstalled package that TC routes because "include system apps" is enabled, a frozen package, or a shared-UID sibling — #229) is therefore still blocked while being absent from the list. AdapterRule's search only searched the already-filtered visible subset (listAll), so searching for the hidden app could never find it either — matching the exact failing repro in #420 (search "Instagram" -> missing). Fix (search-only, keeps the default view uncluttered): - Rule.getSearchStubs(): build a lightweight, side-effect-free list of rules for every installed app (skips tracker counts, default seeding, sorting, snackbars). - AdapterRule: search now runs over the visible list plus every other installed app (lazily built, cached, invalidated on set()), so a hidden-but-filtered app can be found and opened to configure an exception. Visible apps keep their fully-populated rules. Empty query still shows exactly the visible list, so no clutter regression. Deferred (documented, not fixed here): the underlying display-filter / shared-UID (#229) attribution behaviour is left intact; this change only guarantees recoverability, which is the user-facing breakage. Co-Authored-By: Claude Opus 4.8 --- .../eu/faircode/netguard/AdapterRule.java | 44 ++++++++++++++++-- .../main/java/eu/faircode/netguard/Rule.java | 46 +++++++++++++++++++ 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 0c8917055..38e509726 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -62,7 +62,9 @@ import net.kollnig.missioncontrol.data.InternetBlocklist; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class AdapterRule extends RecyclerView.Adapter implements Filterable { private static final String TAG = "TrackerControl.Adapter"; @@ -78,8 +80,13 @@ public class AdapterRule extends RecyclerView.Adapter im private boolean wifiActive = true; private boolean otherActive = true; private boolean live = true; + private final Context appContext; private List listAll = new ArrayList<>(); private List listFiltered = new ArrayList<>(); + // Full installed-app set used only for search, so apps hidden from the visible + // list by the display filters remain reachable (#420, #483). Built lazily and + // invalidated whenever the visible list is replaced. + private List searchPool = null; private int iconSize; private final RequestOptions glideOptions; private String cachedSort; @@ -106,6 +113,7 @@ public ViewHolder(View itemView) { public AdapterRule(Context context, View anchor) { this.anchor = anchor; + this.appContext = context.getApplicationContext(); this.inflater = LayoutInflater.from(context); this.glideOptions = new RequestOptions().format(DecodeFormat.PREFER_RGB_565); @@ -147,6 +155,7 @@ public void updateSortPreference(String sort) { public void set(List listRule) { listAll = listRule; + searchPool = null; // rebuilt lazily on next search List oldList = listFiltered; listFiltered = new ArrayList<>(listRule); DiffUtil.DiffResult result = DiffUtil.calculateDiff(new RuleDiffCallback(oldList, listFiltered)); @@ -379,23 +388,52 @@ private void updateRule(Context context, Rule rule, boolean root, List lis } } + /** + * The set searched by the search box. It is the visible list plus every other installed + * app, so apps hidden by the display filters (system / frozen / no-internet) but still + * routed and filtered by TC can be found and configured (#420, #483). Visible apps keep + * their fully-populated rules (with tracker counts); hidden apps are added as lightweight + * stubs. Built once per visible-list update and cached. + */ + private synchronized List getSearchPool() { + if (searchPool != null) + return searchPool; + + List pool = new ArrayList<>(listAll); + Set present = new HashSet<>(); + for (Rule rule : listAll) + present.add(rule.packageName); + try { + for (Rule stub : Rule.getSearchStubs(appContext)) + if (!present.contains(stub.packageName)) + pool.add(stub); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + searchPool = pool; + return pool; + } + @Override public Filter getFilter() { return new Filter() { @Override protected FilterResults performFiltering(CharSequence query) { List listResult = new ArrayList<>(); - if (query == null) + String queryStr = query == null ? "" : query.toString().toLowerCase().trim(); + if (queryStr.isEmpty()) listResult.addAll(listAll); else { - String queryStr = query.toString().toLowerCase().trim(); int uid; try { uid = Integer.parseInt(queryStr); } catch (NumberFormatException ignore) { uid = -1; } - for (Rule rule : listAll) + // Search the full installed-app set, not just the visible list, so hidden + // but filtered apps stay reachable (#420, #483). + for (Rule rule : getSearchPool()) if (rule.uid == uid || rule.packageName.toLowerCase().contains(queryStr) || (rule.name != null && rule.name.toLowerCase().contains(queryStr))) diff --git a/app/src/main/java/eu/faircode/netguard/Rule.java b/app/src/main/java/eu/faircode/netguard/Rule.java index 3fc5462e4..c11565847 100644 --- a/app/src/main/java/eu/faircode/netguard/Rule.java +++ b/app/src/main/java/eu/faircode/netguard/Rule.java @@ -513,6 +513,52 @@ else if (rule.getTrackerCount(pastWeekOnly) > other.getTrackerCount(pastWeekOnly } } + /** + * Build a lightweight list of rules for ALL installed applications, for search only. + * + * The visible list built by {@link #getRules} applies the show_system / show_frozen / + * show_nointernet display filters. An app hidden by those filters (e.g. an OEM-preinstalled + * app that TC routes because "include system apps" is on, a frozen package, or a shared-UID + * sibling) can still be routed through the tun and filtered/blocked by UID, yet be absent + * from the list and therefore unreachable — the user cannot open it to add an exception or + * exclude it. That is the "blocked but not listed" breakage reported in issues #420 + * (Instagram) and #483 (Feeder), whose only workaround is disabling TC entirely. + * + * Search therefore must reach every installed app, not just the currently-visible subset. + * This intentionally skips the expensive / side-effecting work in getRules (tracker counts, + * default seeding, sorting, snackbars): stubs only need enough to be matched by the search + * filter and to open the per-app details screen (name, package, uid, internet). + */ + public static List getSearchStubs(Context context) { + synchronized (context.getApplicationContext()) { + List listStub = new ArrayList<>(); + + SharedPreferences apply = context.getSharedPreferences("apply", Context.MODE_PRIVATE); + SharedPreferences tracker_protect = context.getSharedPreferences("tracker_protect", Context.MODE_PRIVATE); + SharedPreferences notify = context.getSharedPreferences("notify", Context.MODE_PRIVATE); + + DatabaseHelper dh = DatabaseHelper.getInstance(context); + for (PackageInfo info : getPackages(context)) + try { + // Skip self + if (info.applicationInfo.uid == Process.myUid()) + continue; + + Rule rule = new Rule(dh, info, context); + rule.apply = apply.getBoolean(info.packageName, true); + rule.tracker_protect = BlockingMode.isTrackerProtectionEnabled( + context, tracker_protect, info.packageName); + rule.notify = notify.getBoolean(info.packageName, true); + rule.updateChanged(); + listStub.add(rule); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + return listStub; + } + } + private static List getHandlingPackages(PackageManager pm, Intent intent) { List packagesList = new ArrayList<>(); From 8f77cd433751e5110e565ef170d7d580ce88639f Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Sun, 12 Jul 2026 12:45:40 +0200 Subject: [PATCH 2/2] Use locale-independent app search matching --- app/src/main/java/eu/faircode/netguard/AdapterRule.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 38e509726..29ae9b828 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -64,6 +64,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; public class AdapterRule extends RecyclerView.Adapter implements Filterable { @@ -421,7 +422,7 @@ public Filter getFilter() { @Override protected FilterResults performFiltering(CharSequence query) { List listResult = new ArrayList<>(); - String queryStr = query == null ? "" : query.toString().toLowerCase().trim(); + String queryStr = query == null ? "" : query.toString().toLowerCase(Locale.ROOT).trim(); if (queryStr.isEmpty()) listResult.addAll(listAll); else { @@ -435,8 +436,8 @@ protected FilterResults performFiltering(CharSequence query) { // but filtered apps stay reachable (#420, #483). for (Rule rule : getSearchPool()) if (rule.uid == uid || - rule.packageName.toLowerCase().contains(queryStr) || - (rule.name != null && rule.name.toLowerCase().contains(queryStr))) + rule.packageName.toLowerCase(Locale.ROOT).contains(queryStr) || + (rule.name != null && rule.name.toLowerCase(Locale.ROOT).contains(queryStr))) listResult.add(rule); }