Skip to content

Commit ac9d4e1

Browse files
committed
Fix insights aggregation semantics
1 parent 2343984 commit ac9d4e1

5 files changed

Lines changed: 91 additions & 45 deletions

File tree

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -841,23 +841,21 @@ public long getHostCount(int uid, boolean usecache) {
841841
}
842842

843843
/**
844-
* Get tracking statistics for the past 7 days for the Insights screen.
845-
* Returns aggregated data including blocked/allowed counts per uid and daddr.
846-
*
847-
* @return Cursor with columns: uid, daddr, block, connections, time
844+
* Get recent tracker access rows for the Insights screen.
845+
* The provider deduplicates these rows into the latest app-host contact.
846+
*
847+
* @return Cursor with columns: uid, daddr, allowed, time, uncertain
848848
*/
849849
public Cursor getInsightsData7Days() {
850850
lock.readLock().lock();
851851
try {
852852
SQLiteDatabase db = this.getReadableDatabase();
853853
long sevenDaysAgo = System.currentTimeMillis() - (7L * 24 * 60 * 60 * 1000);
854854

855-
// Query access table for last 7 days, aggregating connections
856-
String query = "SELECT uid, daddr, block, " +
857-
"COALESCE(connections, 1) as connections, time, uncertain " +
855+
String query = "SELECT uid, daddr, allowed, time, uncertain " +
858856
"FROM access " +
859857
"WHERE time >= ? " +
860-
"ORDER BY time DESC";
858+
"ORDER BY time DESC, ID DESC";
861859

862860
return db.rawQuery(query, new String[] { Long.toString(sevenDaysAgo) });
863861
} finally {

app/src/main/java/net/kollnig/missioncontrol/data/InsightsData.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import android.util.Pair
2121

2222
/**
2323
* Data model holding aggregated tracking statistics for the Insights screen.
24-
* Contains 7-day statistics about trackers contacted and blocking effectiveness.
24+
* Contains 7-day statistics about observed tracker-host contacts and blocking effectiveness.
2525
*/
2626
data class InsightsData(
2727
// Overall 7-day summary
@@ -58,4 +58,3 @@ data class InsightsData(
5858
return totalTrackingAttempts > 0 || uniqueTrackerCompanies > 0
5959
}
6060
}
61-

app/src/main/java/net/kollnig/missioncontrol/data/InsightsDataProvider.kt

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.util.Pair
2424
import androidx.preference.PreferenceManager
2525
import eu.faircode.netguard.DatabaseHelper
2626
import net.kollnig.missioncontrol.Common
27+
import java.util.Locale
2728

2829
/**
2930
* Provider class that computes InsightsData from the database.
@@ -53,10 +54,12 @@ class InsightsDataProvider(context: Context) {
5354
val showSystem = prefs.getBoolean("show_system", false)
5455
val applyPrefs = context.getSharedPreferences("apply", Context.MODE_PRIVATE)
5556
val trackerProtectPrefs = context.getSharedPreferences("tracker_protect", Context.MODE_PRIVATE)
57+
val blockingMode = BlockingMode.getMode(context)
5658

5759
// Cache for UID -> package info lookups
5860
val uidPackageCache = mutableMapOf<Int, String?>()
5961
val uidSystemCache = mutableMapOf<Int, Boolean>()
62+
val seenContacts = mutableSetOf<String>()
6063

6164
// Maps for aggregation
6265
val appTrackerCounts = mutableMapOf<Int, Int>() // uid -> unique tracker hosts
@@ -69,15 +72,21 @@ class InsightsDataProvider(context: Context) {
6972

7073
// Top domains: domain -> set of UIDs
7174
val domainToApps = mutableMapOf<String, MutableSet<Int>>()
75+
val uncertainDomains = mutableSetOf<String>()
7276

7377
databaseHelper.getInsightsData7Days().use { cursor ->
7478
if (cursor != null && cursor.moveToFirst()) {
7579
val uidIndex = cursor.getColumnIndexOrThrow("uid")
7680
val daddrIndex = cursor.getColumnIndexOrThrow("daddr")
81+
val allowedIndex = cursor.getColumnIndex("allowed")
82+
val uncertainIndex = cursor.getColumnIndex("uncertain")
7783

7884
do {
7985
val uid = cursor.getInt(uidIndex)
8086
val daddr = cursor.getString(daddrIndex)
87+
val contactKey = "$uid|$daddr"
88+
89+
if (!seenContacts.add(contactKey)) continue
8190

8291
// Get package name (cached)
8392
val packageName = uidPackageCache.getOrPut(uid) {
@@ -101,16 +110,29 @@ class InsightsDataProvider(context: Context) {
101110

102111
// Find tracker company for this hostname
103112
val tracker = TrackerList.findTracker(daddr) ?: continue
113+
val allowed = if (allowedIndex >= 0 && !cursor.isNull(allowedIndex))
114+
cursor.getInt(allowedIndex)
115+
else
116+
-1
117+
val uncertainty = if (uncertainIndex >= 0 && !cursor.isNull(uncertainIndex))
118+
cursor.getInt(uncertainIndex)
119+
else
120+
DatabaseHelper.ACCESS_UNCERTAIN_NONE
104121

105122
val companyName = tracker.name ?: daddr
106123
uniqueCompanies.add(companyName)
107124
appsWithTrackers.add(uid)
108125

109-
// Count tracker hosts seen over the last 7 days.
126+
// Count latest unique app-host contacts seen over the last 7 days.
110127
data.totalTrackingAttempts += 1
111128

112-
// Check if this tracker is currently blocked
113-
val isBlocked = trackerBlocklist.blockedTracker(uid, tracker)
129+
val isBlocked = isTrackerContactBlocked(
130+
uid,
131+
tracker,
132+
allowed,
133+
uncertainty,
134+
blockingMode
135+
)
114136
if (isBlocked) {
115137
data.blockedTrackingAttempts += 1
116138
} else {
@@ -125,9 +147,11 @@ class InsightsDataProvider(context: Context) {
125147

126148
// Track which apps contact each company
127149
companyToApps.getOrPut(companyName) { mutableSetOf() }.add(uid)
128-
150+
129151
// Track which apps contact each domain
130152
domainToApps.getOrPut(daddr) { mutableSetOf() }.add(uid)
153+
if (uncertainty > DatabaseHelper.ACCESS_UNCERTAIN_NONE)
154+
uncertainDomains.add(daddr)
131155

132156
} while (cursor.moveToNext())
133157
}
@@ -164,26 +188,7 @@ class InsightsDataProvider(context: Context) {
164188
// Group by TLD+1 (e.g., ads.google.com -> google.com) to reduce clutter
165189
// For uncertain entries, show alternate tracker domains inline
166190
val tldPlusOneToApps = mutableMapOf<String, MutableSet<Int>>()
167-
168-
// Track which domains are uncertain (need to show alternates)
169-
val uncertainDomains = mutableSetOf<String>()
170-
171-
// First pass: collect all uncertain domains from the cursor data
172-
// Re-query to get uncertain info (since we need fresh cursor)
173-
databaseHelper.getInsightsData7Days().use { cursor ->
174-
if (cursor != null && cursor.moveToFirst()) {
175-
val daddrIndex = cursor.getColumnIndexOrThrow("daddr")
176-
val uncertainIndex = cursor.getColumnIndex("uncertain")
177-
178-
do {
179-
val daddr = cursor.getString(daddrIndex)
180-
if (uncertainIndex >= 0 && cursor.getInt(uncertainIndex) > 0) {
181-
uncertainDomains.add(daddr)
182-
}
183-
} while (cursor.moveToNext())
184-
}
185-
}
186-
191+
187192
for ((daddr, uids) in domainToApps) {
188193
val tldPlusOne = extractTldPlusOne(daddr)
189194

@@ -235,12 +240,16 @@ class InsightsDataProvider(context: Context) {
235240
* Extract TLD+1 from a domain name (e.g., ads.google.com -> google.com)
236241
*/
237242
private fun extractTldPlusOne(domain: String): String {
238-
val parts = domain.split(".")
239-
return if (parts.size >= 2) {
240-
"${parts[parts.size - 2]}.${parts[parts.size - 1]}"
241-
} else {
242-
domain
243-
}
243+
val normalized = domain.trim('.').lowercase(Locale.ROOT)
244+
val parts = normalized.split(".").filter { it.isNotEmpty() }
245+
if (parts.size < 2)
246+
return normalized
247+
248+
val suffix = parts.takeLast(2).joinToString(".")
249+
return if (parts.size >= 3 && MULTI_LABEL_PUBLIC_SUFFIXES.contains(suffix))
250+
"${parts[parts.size - 3]}.$suffix"
251+
else
252+
suffix
244253
}
245254

246255
/**
@@ -270,4 +279,44 @@ class InsightsDataProvider(context: Context) {
270279
tldPlusOne
271280
}
272281
}
282+
283+
private fun isTrackerContactBlocked(
284+
uid: Int,
285+
tracker: Tracker,
286+
allowed: Int,
287+
uncertainty: Int,
288+
blockingMode: String
289+
): Boolean {
290+
if (allowed >= 0)
291+
return allowed == 0
292+
293+
if (!BlockingMode.isStrictMode(context)
294+
&& uncertainty == DatabaseHelper.ACCESS_UNCERTAIN_MIXED_TRACKER_AND_NON_TRACKER) {
295+
return false
296+
}
297+
298+
val blockedByGranularRule = if (BlockingMode.MODE_MINIMAL == blockingMode) {
299+
false
300+
} else {
301+
trackerBlocklist.blockedTracker(uid, tracker)
302+
}
303+
304+
return BlockingModeLogic.shouldBlockKnownTracker(
305+
blockingMode,
306+
tracker.category,
307+
blockedByGranularRule
308+
)
309+
}
310+
311+
companion object {
312+
private val MULTI_LABEL_PUBLIC_SUFFIXES = setOf(
313+
"ac.uk", "co.uk", "gov.uk", "org.uk",
314+
"co.jp", "ne.jp", "or.jp",
315+
"com.au", "edu.au", "gov.au", "net.au", "org.au",
316+
"ac.nz", "co.nz", "govt.nz", "org.nz",
317+
"co.in", "firm.in", "gen.in", "ind.in", "net.in", "org.in",
318+
"com.br", "com.cn", "com.hk", "com.mx", "com.my", "com.sg",
319+
"com.tr", "com.tw", "net.cn", "org.cn"
320+
)
321+
}
273322
}

app/src/main/res/layout/layout_insights_share.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
<TextView
133133
android:layout_width="wrap_content"
134134
android:layout_height="wrap_content"
135-
android:text="@string/insights_top_companies"
135+
android:text="@string/insights_pervasive_trackers"
136136
android:textColor="@android:color/white"
137137
android:textSize="14sp"
138138
android:textStyle="bold"

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,11 +613,11 @@ Sincerely,\n\n]]></string>
613613
<string name="insights_shocking_fact">Your apps contacted %1$d tracking companies via %2$d tracker hosts this week!</string>
614614
<string name="insights_shocking_fact_playstore">Your apps contacted %1$d tracking companies this week!</string>
615615
<string name="insights_share">Share with Friends</string>
616-
<string name="insights_share_trackers_blocked">Trackers Blocked</string>
617-
<string name="insights_share_trackers_detected">Trackers Detected</string>
616+
<string name="insights_share_trackers_blocked">Tracker Hosts Blocked</string>
617+
<string name="insights_share_trackers_detected">Tracker Hosts Detected</string>
618618
<string name="insights_share_url">TrackerControl.org</string>
619-
<string name="insights_share_message">I blocked %1$d trackers from %2$d companies this week using TrackerControl! Take back your privacy: https://trackercontrol.org</string>
620-
<string name="insights_share_message_playstore">I detected %1$d trackers from %2$d companies this week using TrackerControl! Take back your privacy: https://trackercontrol.org</string>
619+
<string name="insights_share_message">I blocked %1$d tracker hosts from %2$d companies this week using TrackerControl! Take back your privacy: https://trackercontrol.org</string>
620+
<string name="insights_share_message_playstore">I detected %1$d tracker hosts from %2$d companies this week using TrackerControl! Take back your privacy: https://trackercontrol.org</string>
621621
<string name="insights_see_more">See More →</string>
622622

623623
<!-- What's New onboarding slide (not translated) -->

0 commit comments

Comments
 (0)