From 82dcecc689f243d78e4aaad6416b66c1c6fd0789 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:33:47 +0000 Subject: [PATCH 1/3] Initial plan From 81ead947fa857b5d104485d4c2e9e0369e6a994c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:39:40 +0000 Subject: [PATCH 2/3] Optimize nested loops and string concatenation for better performance Co-authored-by: kasnder <5175206+kasnder@users.noreply.github.com> --- .../eu/faircode/netguard/AdapterRule.java | 16 +- .../eu/faircode/netguard/DatabaseHelper.java | 161 ++++++++++-------- .../missioncontrol/data/BlocklistManager.java | 16 +- 3 files changed, 107 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/eu/faircode/netguard/AdapterRule.java b/app/src/main/java/eu/faircode/netguard/AdapterRule.java index 2ffc88e8..1065732f 100644 --- a/app/src/main/java/eu/faircode/netguard/AdapterRule.java +++ b/app/src/main/java/eu/faircode/netguard/AdapterRule.java @@ -1039,10 +1039,19 @@ private void updateRule(Context context, Rule rule, boolean root, List lis rule.updateChanged(context); Log.i(TAG, "Updated " + rule); + // Optimize: Use HashMap to avoid O(n*m) nested loop complexity List listModified = new ArrayList<>(); - for (String pkg : rule.related) { - for (Rule related : listAll) - if (related.packageName.equals(pkg)) { + if (!rule.related.isEmpty()) { + // Build a map for O(1) lookup + java.util.HashMap packageMap = new java.util.HashMap<>(); + for (Rule r : listAll) { + packageMap.put(r.packageName, r); + } + + // Find and update related rules in O(n) time + for (String pkg : rule.related) { + Rule related = packageMap.get(pkg); + if (related != null) { related.wifi_blocked = rule.wifi_blocked; related.other_blocked = rule.other_blocked; related.apply = rule.apply; @@ -1053,6 +1062,7 @@ private void updateRule(Context context, Rule rule, boolean root, List lis related.notify = rule.notify; listModified.add(related); } + } } List listSearch = (root ? new ArrayList<>(listAll) : listAll); diff --git a/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java b/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java index 58567186..5fb8febe 100644 --- a/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java +++ b/app/src/main/java/eu/faircode/netguard/DatabaseHelper.java @@ -501,23 +501,24 @@ public Cursor getLog(boolean udp, boolean tcp, boolean other, boolean allowed, b SQLiteDatabase db = this.getReadableDatabase(); // There is an index on time // There is no index on protocol/allowed for write performance - String query = "SELECT ID AS _id, *"; - query += " FROM log"; - query += " WHERE (0 = 1"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM log"); + query.append(" WHERE (0 = 1"); if (udp) - query += " OR protocol = 17"; + query.append(" OR protocol = 17"); if (tcp) - query += " OR protocol = 6"; + query.append(" OR protocol = 6"); if (other) - query += " OR (protocol <> 6 AND protocol <> 17)"; - query += ") AND (0 = 1"; + query.append(" OR (protocol <> 6 AND protocol <> 17)"); + query.append(") AND (0 = 1"); if (allowed) - query += " OR allowed = 1"; + query.append(" OR allowed = 1"); if (blocked) - query += " OR allowed = 0"; - query += ")"; - query += " ORDER BY time DESC"; - return db.rawQuery(query, new String[] {}); + query.append(" OR allowed = 0"); + query.append(")"); + query.append(" ORDER BY time DESC"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } @@ -528,11 +529,12 @@ public Cursor searchLog(String find) { try { SQLiteDatabase db = this.getReadableDatabase(); // There is an index on daddr, dname, dport and uid - String query = "SELECT ID AS _id, *"; - query += " FROM log"; - query += " WHERE daddr LIKE ? OR dname LIKE ? OR dport = ? OR uid = ?"; - query += " ORDER BY time DESC"; - return db.rawQuery(query, new String[] { "%" + find + "%", "%" + find + "%", find, find }); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM log"); + query.append(" WHERE daddr LIKE ? OR dname LIKE ? OR dport = ? OR uid = ?"); + query.append(" ORDER BY time DESC"); + return db.rawQuery(query.toString(), new String[] { "%" + find + "%", "%" + find + "%", find, find }); } finally { lock.readLock().unlock(); } @@ -740,13 +742,14 @@ public Cursor getAccess(int uid) { SQLiteDatabase db = this.getReadableDatabase(); // There is a segmented index on uid // There is no index on time for write performance - String query = "SELECT a.ID AS _id, a.*"; - query += ", (SELECT COUNT(DISTINCT d.qname) FROM dns d WHERE d.resource IN (SELECT d1.resource FROM dns d1 WHERE d1.qname = a.daddr)) count"; - query += " FROM access a"; - query += " WHERE a.uid = ?"; - query += " ORDER BY a.time DESC"; - query += " LIMIT 250"; - return db.rawQuery(query, new String[] { Integer.toString(uid) }); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT a.ID AS _id, a.*"); + query.append(", (SELECT COUNT(DISTINCT d.qname) FROM dns d WHERE d.resource IN (SELECT d1.resource FROM dns d1 WHERE d1.qname = a.daddr)) count"); + query.append(" FROM access a"); + query.append(" WHERE a.uid = ?"); + query.append(" ORDER BY a.time DESC"); + query.append(" LIMIT 250"); + return db.rawQuery(query.toString(), new String[] { Integer.toString(uid) }); } finally { lock.readLock().unlock(); } @@ -770,16 +773,17 @@ public Cursor getAccessUnset(int uid, int limit, long since) { SQLiteDatabase db = this.getReadableDatabase(); // There is a segmented index on uid, block and daddr // There is no index on allowed and time for write performance - String query = "SELECT MAX(time) AS time, daddr, allowed"; - query += " FROM access"; - query += " WHERE uid = ?"; - query += " AND block < 0"; - query += " AND time >= ?"; - query += " GROUP BY daddr, allowed"; - query += " ORDER BY time DESC"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT MAX(time) AS time, daddr, allowed"); + query.append(" FROM access"); + query.append(" WHERE uid = ?"); + query.append(" AND block < 0"); + query.append(" AND time >= ?"); + query.append(" GROUP BY daddr, allowed"); + query.append(" ORDER BY time DESC"); if (limit > 0) - query += " LIMIT " + limit; - return db.rawQuery(query, new String[] { Integer.toString(uid), Long.toString(since) }); + query.append(" LIMIT ").append(limit); + return db.rawQuery(query.toString(), new String[] { Integer.toString(uid), Long.toString(since) }); } finally { lock.readLock().unlock(); } @@ -923,14 +927,15 @@ public String getQName(int uid, String ip) { readableDb = this.getReadableDatabase(); SQLiteDatabase db = readableDb; // There is a segmented index on resource - String query = "SELECT d.qname"; - query += " FROM dns AS d"; - query += " WHERE d.resource = '" + ip.replace("'", "''") + "'"; - query += " ORDER BY d.qname"; - query += " LIMIT 1"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT d.qname"); + query.append(" FROM dns AS d"); + query.append(" WHERE d.resource = '").append(ip.replace("'", "''")).append("'"); + query.append(" ORDER BY d.qname"); + query.append(" LIMIT 1"); // There is no way to known for sure which domain name an app used, so just pick // the first one - return db.compileStatement(query).simpleQueryForString(); + return db.compileStatement(query.toString()).simpleQueryForString(); } catch (SQLiteDoneException ignored) { // Not found return null; @@ -947,14 +952,15 @@ public Cursor getQAName(int uid, String ip, boolean alive) { readableDb = this.getReadableDatabase(); SQLiteDatabase db = readableDb; // There is a segmented index on resource - String query = "SELECT d.qname, d.aname, d.time, d.ttl"; - query += " FROM dns AS d"; - query += " WHERE d.resource = '" + ip.replace("'", "''") + "'"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT d.qname, d.aname, d.time, d.ttl"); + query.append(" FROM dns AS d"); + query.append(" WHERE d.resource = '").append(ip.replace("'", "''")).append("'"); if (alive) - query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; - query += " GROUP BY d.qname"; // remove duplicates - query += " ORDER BY d.qname"; - return db.rawQuery(query, new String[] {}); + query.append(" AND (d.time IS NULL OR d.time + d.ttl >= ").append(now).append(")"); + query.append(" GROUP BY d.qname"); // remove duplicates + query.append(" ORDER BY d.qname"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } @@ -964,13 +970,14 @@ public Cursor getAlternateQNames(String qname) { lock.readLock().lock(); try { SQLiteDatabase db = this.getReadableDatabase(); - String query = "SELECT DISTINCT d2.qname"; - query += " FROM dns d1"; - query += " JOIN dns d2"; - query += " ON d2.resource = d1.resource AND d2.id <> d1.id"; - query += " WHERE d1.qname = ?"; - query += " ORDER BY d2.qname"; - return db.rawQuery(query, new String[] { qname }); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT DISTINCT d2.qname"); + query.append(" FROM dns d1"); + query.append(" JOIN dns d2"); + query.append(" ON d2.resource = d1.resource AND d2.id <> d1.id"); + query.append(" WHERE d1.qname = ?"); + query.append(" ORDER BY d2.qname"); + return db.rawQuery(query.toString(), new String[] { qname }); } finally { lock.readLock().unlock(); } @@ -981,13 +988,14 @@ public Cursor getAName(String qname, boolean alive) { lock.readLock().lock(); try { SQLiteDatabase db = this.getReadableDatabase(); - String query = "SELECT d.qname, d.aname, d.time, d.ttl"; - query += " FROM dns d"; - query += " WHERE d.qname = ?"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT d.qname, d.aname, d.time, d.ttl"); + query.append(" FROM dns d"); + query.append(" WHERE d.qname = ?"); if (alive) - query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; - query += " LIMIT 1"; - return db.rawQuery(query, new String[] { qname }); + query.append(" AND (d.time IS NULL OR d.time + d.ttl >= ").append(now).append(")"); + query.append(" LIMIT 1"); + return db.rawQuery(query.toString(), new String[] { qname }); } finally { lock.readLock().unlock(); } @@ -999,10 +1007,11 @@ public Cursor getDns() { SQLiteDatabase db = this.getReadableDatabase(); // There is an index on resource // There is a segmented index on qname - String query = "SELECT ID AS _id, *"; - query += " FROM dns"; - query += " ORDER BY resource, qname"; - return db.rawQuery(query, new String[] {}); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM dns"); + query.append(" ORDER BY resource, qname"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } @@ -1016,16 +1025,17 @@ public Cursor getAccessDns(String dname) { // There is a segmented index on dns.qname // There is an index on access.daddr and access.block - String query = "SELECT a.uid, a.version, a.protocol, a.daddr, d.resource, a.dport, a.block, d.time, d.ttl"; - query += " FROM access AS a"; - query += " LEFT JOIN dns AS d"; - query += " ON d.qname = a.daddr"; - query += " WHERE a.block >= 0"; - query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT a.uid, a.version, a.protocol, a.daddr, d.resource, a.dport, a.block, d.time, d.ttl"); + query.append(" FROM access AS a"); + query.append(" LEFT JOIN dns AS d"); + query.append(" ON d.qname = a.daddr"); + query.append(" WHERE a.block >= 0"); + query.append(" AND (d.time IS NULL OR d.time + d.ttl >= ").append(now).append(")"); if (dname != null) - query += " AND a.daddr = ?"; + query.append(" AND a.daddr = ?"); - return db.rawQuery(query, dname == null ? new String[] {} : new String[] { dname }); + return db.rawQuery(query.toString(), dname == null ? new String[] {} : new String[] { dname }); } finally { lock.readLock().unlock(); } @@ -1103,10 +1113,11 @@ public Cursor getForwarding() { lock.readLock().lock(); try { SQLiteDatabase db = this.getReadableDatabase(); - String query = "SELECT ID AS _id, *"; - query += " FROM forward"; - query += " ORDER BY dport"; - return db.rawQuery(query, new String[] {}); + // Optimize: Use StringBuilder for efficient string concatenation + StringBuilder query = new StringBuilder("SELECT ID AS _id, *"); + query.append(" FROM forward"); + query.append(" ORDER BY dport"); + return db.rawQuery(query.toString(), new String[] {}); } finally { lock.readLock().unlock(); } diff --git a/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java b/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java index f9d2f56d..61ea6b4d 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java +++ b/app/src/main/java/net/kollnig/missioncontrol/data/BlocklistManager.java @@ -41,16 +41,16 @@ private void cleanup() { if (files == null) return; + // Optimize: Use HashSet to avoid O(n*m) nested loop complexity List list = getBlocklists(); + java.util.HashSet validFiles = new java.util.HashSet<>(); + for (Blocklist item : list) { + validFiles.add("blocklist_" + item.uuid + ".txt"); + } + + // Check each file in O(1) time using HashSet for (File file : files) { - boolean found = false; - for (Blocklist item : list) { - if (file.getName().equals("blocklist_" + item.uuid + ".txt")) { - found = true; - break; - } - } - if (!found) { + if (!validFiles.contains(file.getName())) { Log.i(TAG, "Deleting orphaned file " + file.getName()); file.delete(); } From fc9aea5ffd0df31c4c605fc64c70b186b7d5d24f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:41:30 +0000 Subject: [PATCH 3/3] Cache sorted hosts in Tracker class to avoid repeated sorting in UI Co-authored-by: kasnder <5175206+kasnder@users.noreply.github.com> --- .../kollnig/missioncontrol/data/Tracker.java | 20 +++++++++++++++++++ .../details/TrackersListAdapter.java | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java b/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java index 3cfaab02..9199f510 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java +++ b/app/src/main/java/net/kollnig/missioncontrol/data/Tracker.java @@ -19,7 +19,10 @@ import androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -27,6 +30,7 @@ */ public class Tracker { private final Set hosts = new HashSet<>(); + private List sortedHostsCache = null; // Cache sorted hosts for performance public String name; public String category; public Long lastSeen; @@ -97,6 +101,8 @@ public String getCategory() { */ void addHost(String host) { this.hosts.add(host); + // Invalidate cache when host is added + sortedHostsCache = null; } /** @@ -107,4 +113,18 @@ void addHost(String host) { public Set getHosts() { return hosts; } + + /** + * Get sorted list of hosts. This method caches the sorted result for performance. + * Calling this repeatedly is more efficient than sorting in UI code. + * + * @return Sorted list of hosts + */ + public List getSortedHosts() { + if (sortedHostsCache == null) { + sortedHostsCache = new ArrayList<>(hosts); + Collections.sort(sortedHostsCache); + } + return sortedHostsCache; + } } diff --git a/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java b/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java index 532c78e4..6006597a 100644 --- a/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java +++ b/app/src/main/java/net/kollnig/missioncontrol/details/TrackersListAdapter.java @@ -276,8 +276,8 @@ private void updateText(TextView tv, Tracker t) { if (t.lastSeen != 0) title += " (" + Util.relativeTime(t.lastSeen) + ")"; - List sortedHosts = new ArrayList<>(t.getHosts()); - Collections.sort(sortedHosts); + // Optimize: Use cached sorted hosts instead of creating new list and sorting every time + List sortedHosts = t.getSortedHosts(); String hosts = TextUtils.join("\n• ", sortedHosts); boolean categoryBlocked = b.blocked(mAppUid, trackerCategoryName);