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..882945461 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -60,9 +60,13 @@ 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.Locale; +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 +82,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 +111,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 +408,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() { @@ -411,18 +422,36 @@ protected FilterResults performFiltering(CharSequence query) { if (query == null) listResult.addAll(listAll); else { - String queryStr = query.toString().toLowerCase().trim(); - int uid; - try { - uid = Integer.parseInt(queryStr); - } catch (NumberFormatException ignore) { - uid = -1; + 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 + // 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(Locale.ROOT).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