From b73bfcaa56a154b9aea289b246c432743b201fcc Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:34:37 +0200 Subject: [PATCH 1/2] Add tracker: app-list search (#447) Let users find which installed apps have contacted a particular tracker company (e.g. "tracker:gravy analytics" or "tracker:facebook") directly from the existing app-list search box. - TrackerList.getTrackerNamesByUid(): one DB scan building a UID -> contacted-company-names lookup, mirroring the existing getTrackerCountsAndTotal() traversal. - AdapterRule filter: a query starting with "tracker:" matches apps whose observed tracker companies contain the query substring (case-insensitive). The lookup is built once per filter invocation on the Filter's background thread, not per row. Plain queries keep the existing name/package/uid behaviour. - Discoverability: SearchView query hint now reads "Search app, or tracker:company" (new base-English string only). Co-Authored-By: Claude Fable 5 --- .../eu/faircode/netguard/ActivityMain.java | 3 ++ .../eu/faircode/netguard/AdapterRule.java | 48 +++++++++++++++---- .../missioncontrol/data/TrackerList.java | 26 ++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/ActivityMain.java b/app/src/main/java/eu/faircode/netguard/ActivityMain.java index af7bd1744..f91932496 100644 --- a/app/src/main/java/eu/faircode/netguard/ActivityMain.java +++ b/app/src/main/java/eu/faircode/netguard/ActivityMain.java @@ -911,6 +911,9 @@ public boolean onMenuItemActionCollapse(MenuItem item) { }); final SearchView searchView = (SearchView) menuSearch.getActionView(); + // Hint at the "tracker:" search prefix (#447), which filters apps + // by the tracker companies they have been observed contacting. + searchView.setQueryHint(getString(R.string.menu_search_hint)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 44c317e68..9d8387396 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -60,9 +60,12 @@ import net.kollnig.missioncontrol.R; import net.kollnig.missioncontrol.data.BlockingMode; import net.kollnig.missioncontrol.data.InternetBlocklist; +import net.kollnig.missioncontrol.data.TrackerList; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; public class AdapterRule extends RecyclerView.Adapter implements Filterable { private static final String TAG = "TrackerControl.Adapter"; @@ -78,6 +81,7 @@ 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<>(); private int iconSize; @@ -106,6 +110,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); @@ -402,6 +407,11 @@ private void updateRule(Context context, Rule rule, boolean root, List lis } } + // Prefix that switches the search box from matching app name/package/uid to + // matching the tracker companies an app has been observed contacting, e.g. + // "tracker:gravy analytics" (#447). + private static final String TRACKER_SEARCH_PREFIX = "tracker:"; + @Override public Filter getFilter() { return new Filter() { @@ -412,17 +422,35 @@ protected FilterResults performFiltering(CharSequence query) { listResult.addAll(listAll); else { String queryStr = query.toString().toLowerCase().trim(); - int uid; - try { - uid = Integer.parseInt(queryStr); - } catch (NumberFormatException ignore) { - uid = -1; + if (queryStr.startsWith(TRACKER_SEARCH_PREFIX)) { + String company = queryStr.substring(TRACKER_SEARCH_PREFIX.length()).trim(); + // Built once per filter invocation (single DB scan), not per + // row, and runs on this Filter's background thread. + Map> trackersByUid = + TrackerList.getInstance(appContext).getTrackerNamesByUid(); + for (Rule rule : listAll) { + Set companies = trackersByUid.get(rule.uid); + if (companies == null) + continue; + for (String name : companies) + if (name != null && name.toLowerCase().contains(company)) { + listResult.add(rule); + break; + } + } + } else { + int uid; + try { + uid = Integer.parseInt(queryStr); + } catch (NumberFormatException ignore) { + uid = -1; + } + for (Rule rule : listAll) + if (rule.uid == uid || + rule.packageName.toLowerCase().contains(queryStr) || + (rule.name != null && rule.name.toLowerCase().contains(queryStr))) + listResult.add(rule); } - for (Rule rule : listAll) - if (rule.uid == uid || - rule.packageName.toLowerCase().contains(queryStr) || - (rule.name != null && rule.name.toLowerCase().contains(queryStr))) - listResult.add(rule); } FilterResults result = new FilterResults(); diff --git a/app/src/main/java/net/kollnig/missioncontrol/data/TrackerList.java b/app/src/main/java/net/kollnig/missioncontrol/data/TrackerList.java index c3077c684..d8cf130d1 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/data/TrackerList.java +++ b/app/src/main/java/net/kollnig/missioncontrol/data/TrackerList.java @@ -238,6 +238,32 @@ public synchronized void invalidateTrackerCountCache() { cachedTrackerCounts = null; } + /** + * Retrieves the set of contacted tracker company names, per app UID (#447), so + * the app-list search can filter apps by "tracker:". A single DB scan + * builds a lookup for every app at once, instead of querying per row; the + * caller (AdapterRule's search Filter) already runs on a background thread, so + * no caching is needed here. + * + * @return Map of app UID to the set of tracker company names observed for it + */ + public synchronized Map> getTrackerNamesByUid() { + Map> trackers = new ArrayMap<>(); + + try (Cursor cursor = databaseHelper.getHosts()) { + if (cursor.moveToFirst()) { + do { + int appUid = cursor.getInt(cursor.getColumnIndexOrThrow("uid")); + String hostname = cursor.getString(cursor.getColumnIndexOrThrow("daddr")); + Tracker tracker = findTracker(hostname); + checkTracker(trackers, appUid, tracker); + } while (cursor.moveToNext()); + } + } + + return trackers; + } + /** * Helper method to check tracker * diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9a6ef078..d9630c6d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Access notifications Search for app + Search app, or tracker:company Filter apps Show user apps Show system apps From 6796e5989ee6d4d6db37f76f9a989a95e29c12c8 Mon Sep 17 00:00:00 2001 From: Konrad Kollnig <5175206+kasnder@users.noreply.github.com> Date: Sun, 12 Jul 2026 12:19:22 +0200 Subject: [PATCH 2/2] Use locale-independent tracker search matching --- app/src/main/java/eu/faircode/netguard/AdapterRule.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 9d8387396..882945461 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.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -421,7 +422,7 @@ protected FilterResults performFiltering(CharSequence query) { if (query == null) listResult.addAll(listAll); else { - String queryStr = query.toString().toLowerCase().trim(); + String queryStr = query.toString().toLowerCase(Locale.ROOT).trim(); if (queryStr.startsWith(TRACKER_SEARCH_PREFIX)) { String company = queryStr.substring(TRACKER_SEARCH_PREFIX.length()).trim(); // Built once per filter invocation (single DB scan), not per @@ -433,7 +434,7 @@ protected FilterResults performFiltering(CharSequence query) { if (companies == null) continue; for (String name : companies) - if (name != null && name.toLowerCase().contains(company)) { + if (name != null && name.toLowerCase(Locale.ROOT).contains(company)) { listResult.add(rule); break; }