diff options
Diffstat (limited to 'trackers')
10 files changed, 288 insertions, 135 deletions
diff --git a/trackers/build.gradle b/trackers/build.gradle index f888acf..ecf95be 100644 --- a/trackers/build.gradle +++ b/trackers/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation( Libs.Kotlin.stdlib, Libs.AndroidX.coreKtx, - Libs.Coroutines.core + Libs.Coroutines.core, + Libs.timber ) } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt index 737aa4a..44793a4 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -135,7 +136,6 @@ class DNSBlockerRunnable( private fun shouldBlock(appUid: Int, trackerId: String?): Boolean { return whitelistRepository.isBlockingEnabled && - !whitelistRepository.isAppWhiteListed(appUid) && - !whitelistRepository.isTrackerWhiteListedForApp(trackerId, appUid) + !whitelistRepository.isWhiteListed(appUid, trackerId) } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt index 99e2148..f3c4745 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -39,8 +40,8 @@ class TrackersLogger(context: Context) { stopped = true } - fun logAccess(trackerId: String?, appId: Int, wasBlocked: Boolean) { - queue.offer(DetectedTracker(trackerId, appId, wasBlocked)) + fun logAccess(trackerId: String?, appUid: Int, wasBlocked: Boolean) { + queue.offer(DetectedTracker(trackerId, appUid, wasBlocked)) } private fun startWriteLogLoop() { diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt index 25f0f2a..7463b22 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -18,6 +19,7 @@ package foundation.e.privacymodules.trackers.api import android.content.Context +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.data.TrackersRepository import foundation.e.privacymodules.trackers.data.WhitelistRepository @@ -52,14 +54,14 @@ class BlockTrackersPrivacyModule(context: Context) : IBlockTrackersPrivacyModule mListeners.forEach { listener -> listener.onBlockingToggle(true) } } - override fun getWhiteList(appUid: Int): List<Tracker> { - return whitelistRepository.getWhiteListForApp(appUid).mapNotNull { + override fun getWhiteList(app: ApplicationDescription): List<Tracker> { + return whitelistRepository.getWhiteListForApp(app).mapNotNull { trackersRepository.getTracker(it) } } - override fun getWhiteListedApp(): List<Int> { - return whitelistRepository.whiteListedApp + override fun getWhiteListedApp(): List<ApplicationDescription> { + return whitelistRepository.getWhiteListedApp() } override fun isBlockingEnabled(): Boolean { @@ -70,19 +72,27 @@ class BlockTrackersPrivacyModule(context: Context) : IBlockTrackersPrivacyModule return whitelistRepository.areWhiteListEmpty() } - override fun isWhitelisted(appUid: Int): Boolean { - return whitelistRepository.isAppWhiteListed(appUid) + override fun isWhitelisted(app: ApplicationDescription): Boolean { + return whitelistRepository.isAppWhiteListed(app) } override fun removeListener(listener: IBlockTrackersPrivacyModule.Listener) { mListeners.remove(listener) } - override fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) { - whitelistRepository.setWhiteListed(tracker, appUid, isWhiteListed) + override fun setWhiteListed( + tracker: Tracker, + app: ApplicationDescription, + isWhiteListed: Boolean + ) { + whitelistRepository.setWhiteListed(tracker, app.apId, isWhiteListed) } - override fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) { - whitelistRepository.setWhiteListed(appUid, isWhiteListed) + override fun setWhiteListed(app: ApplicationDescription, isWhiteListed: Boolean) { + whitelistRepository.setWhiteListed(app.apId, isWhiteListed) + } + + override fun clearWhiteList(app: ApplicationDescription) { + whitelistRepository.clearWhiteList(app.apId) } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt index 9e1a041..3547b8e 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -17,6 +18,8 @@ package foundation.e.privacymodules.trackers.api +import foundation.e.privacymodules.permissions.data.ApplicationDescription + /** * Manage trackers blocking and whitelisting. */ @@ -40,18 +43,18 @@ interface IBlockTrackersPrivacyModule { /** * Set or unset in whitelist the App with the specified uid. - * @param appUid the uid of the app + * @param app the ApplicationDescription of the app * @param isWhiteListed true, the app will appears in whitelist, false, it won't */ - fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) + fun setWhiteListed(app: ApplicationDescription, isWhiteListed: Boolean) /** * Set or unset in whitelist the specifid tracked, for the App specified by its uid. * @param tracker the tracker - * @param appUid the uid of the app + * @param app the ApplicationDescription of the app * @param isWhiteListed true, the app will appears in whitelist, false, it won't */ - fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) + fun setWhiteListed(tracker: Tracker, app: ApplicationDescription, isWhiteListed: Boolean) /** * Return true if nothing has been added to the whitelist : everything is blocked. @@ -61,17 +64,19 @@ interface IBlockTrackersPrivacyModule { /** * Return the white listed App, by their UID */ - fun getWhiteListedApp(): List<Int> + fun getWhiteListedApp(): List<ApplicationDescription> /** * Return true if the App is whitelisted for trackers blocking. */ - fun isWhitelisted(appUid: Int): Boolean + fun isWhitelisted(app: ApplicationDescription): Boolean /** * List the white listed trackers for an App specified by it uid */ - fun getWhiteList(appUid: Int): List<Tracker> + fun getWhiteList(app: ApplicationDescription): List<Tracker> + + fun clearWhiteList(app: ApplicationDescription) /** * Callback interface to get updates about the state of the Block trackers module. diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt index 264f247..8aaed4a 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -17,34 +18,41 @@ package foundation.e.privacymodules.trackers.api +import foundation.e.privacymodules.permissions.data.ApplicationDescription + /** * Get reporting about trackers calls. */ interface ITrackTrackersPrivacyModule { - fun start(trackers: List<Tracker>, enableNotification: Boolean = true) + fun start( + trackers: List<Tracker>, + getAppByUid: (Int) -> ApplicationDescription?, + getAppByAPId: (String) -> ApplicationDescription?, + enableNotification: Boolean = true + ) /** * List all the trackers encountered for a specific app. */ - fun getTrackersForApp(appUid: Int): List<Tracker> + fun getTrackersForApp(app: ApplicationDescription): List<Tracker> /** * List all the trackers encountere trackers since "ever", for the given [appUids], * or all apps if [appUids] is null */ - fun getTrackers(appUids: List<Int>? = null): List<Tracker> + fun getTrackers(apps: List<ApplicationDescription>? = null): List<Tracker> /** * Return the number of encountered trackers since "ever", for the given [appUids], * or all apps if [appUids] is null */ - fun getTrackersCount(appUids: List<Int>? = null): Int + fun getTrackersCount(): Int /** * Return the number of encountere trackers since "ever", for each app uid. */ - fun getTrackersCountByApp(): Map<Int, Int> + fun getTrackersCountByApp(): Map<ApplicationDescription, Int> /** * Return the number of encountered trackers for the last 24 hours @@ -79,11 +87,11 @@ interface ITrackTrackersPrivacyModule { */ fun getPastYearTrackersCalls(): List<Pair<Int, Int>> - fun getPastDayTrackersCallsByApps(): Map<Int, Pair<Int, Int>> + fun getPastDayTrackersCallsByApps(): Map<ApplicationDescription, Pair<Int, Int>> - fun getPastDayTrackersCallsForApp(appUid: Int): Pair<Int, Int> + fun getPastDayTrackersCallsForApp(app: ApplicationDescription): Pair<Int, Int> - fun getPastDayMostLeakedApp(): Int + fun getPastDayMostLeakedApp(): ApplicationDescription? interface Listener { diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt index 18c56c9..5fc5b6b 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -19,9 +20,11 @@ package foundation.e.privacymodules.trackers.api import android.content.Context import android.content.Intent +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.DNSBlockerService import foundation.e.privacymodules.trackers.data.StatsRepository import foundation.e.privacymodules.trackers.data.TrackersRepository +import foundation.e.privacymodules.trackers.data.WhitelistRepository import java.time.temporal.ChronoUnit class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersPrivacyModule { @@ -42,8 +45,15 @@ class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersP } } - override fun start(trackers: List<Tracker>, enableNotification: Boolean) { + override fun start( + trackers: List<Tracker>, + getAppByUid: (Int) -> ApplicationDescription?, + getAppByAPId: (String) -> ApplicationDescription?, + enableNotification: Boolean + ) { TrackersRepository.getInstance().setTrackersList(trackers) + StatsRepository.getInstance(context).setAppGetters(getAppByUid, getAppByAPId) + WhitelistRepository.getInstance(context).setAppGetters(context, getAppByAPId, getAppByUid) val intent = Intent(context, DNSBlockerService::class.java) intent.action = DNSBlockerService.ACTION_START intent.putExtra(DNSBlockerService.EXTRA_ENABLE_NOTIFICATION, enableNotification) @@ -62,20 +72,20 @@ class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersP return statsRepository.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS) } - override fun getTrackersCount(appUids: List<Int>?): Int { - return statsRepository.getContactedTrackersCount(appUids) + override fun getTrackersCount(): Int { + return statsRepository.getContactedTrackersCount() } - override fun getTrackersCountByApp(): Map<Int, Int> { + override fun getTrackersCountByApp(): Map<ApplicationDescription, Int> { return statsRepository.getContactedTrackersCountByApp() } - override fun getTrackersForApp(appUid: Int): List<Tracker> { - return statsRepository.getTrackers(listOf(appUid)) + override fun getTrackersForApp(app: ApplicationDescription): List<Tracker> { + return statsRepository.getTrackers(listOf(app)) } - override fun getTrackers(appUids: List<Int>?): List<Tracker> { - return statsRepository.getTrackers(appUids) + override fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> { + return statsRepository.getTrackers(apps) } override fun getPastDayTrackersCount(): Int { @@ -90,16 +100,16 @@ class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersP return statsRepository.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS) } - override fun getPastDayMostLeakedApp(): Int { + override fun getPastDayMostLeakedApp(): ApplicationDescription? { return statsRepository.getMostLeakedApp(24, ChronoUnit.HOURS) } - override fun getPastDayTrackersCallsByApps(): Map<Int, Pair<Int, Int>> { + override fun getPastDayTrackersCallsByApps(): Map<ApplicationDescription, Pair<Int, Int>> { return statsRepository.getCallsByApps(24, ChronoUnit.HOURS) } - override fun getPastDayTrackersCallsForApp(appUid: Int): Pair<Int, Int> { - return statsRepository.getCalls(appUid, 24, ChronoUnit.HOURS) + override fun getPastDayTrackersCallsForApp(app: ApplicationDescription): Pair<Int, Int> { + return statsRepository.getCalls(app, 24, ChronoUnit.HOURS) } override fun addListener(listener: ITrackTrackersPrivacyModule.Listener) { diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt index 21edb56..4d287d4 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -23,13 +24,15 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.provider.BaseColumns +import androidx.core.database.getStringOrNull import foundation.e.privacymodules.trackers.api.Tracker -import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APP_UID +import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APPID import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_BLOCKED import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_CONTACTED import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TIMESTAMP import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TRACKER import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.TABLE_NAME +import timber.log.Timber import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -40,38 +43,44 @@ class StatsDatabase(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { companion object { - const val DATABASE_VERSION = 1 + const val DATABASE_VERSION = 2 const val DATABASE_NAME = "TrackerFilterStats.db" private const val SQL_CREATE_TABLE = "CREATE TABLE $TABLE_NAME (" + "${BaseColumns._ID} INTEGER PRIMARY KEY," + "$COLUMN_NAME_TIMESTAMP INTEGER," + - "$COLUMN_NAME_APP_UID INTEGER," + "$COLUMN_NAME_TRACKER TEXT," + "$COLUMN_NAME_NUMBER_CONTACTED INTEGER," + - "$COLUMN_NAME_NUMBER_BLOCKED INTEGER)" + "$COLUMN_NAME_NUMBER_BLOCKED INTEGER," + + "$COLUMN_NAME_APPID TEXT)" private const val PROJECTION_NAME_PERIOD = "period" private const val PROJECTION_NAME_CONTACTED_SUM = "contactedsum" private const val PROJECTION_NAME_BLOCKED_SUM = "blockedsum" private const val PROJECTION_NAME_LEAKED_SUM = "leakedsum" private const val PROJECTION_NAME_TRACKERS_COUNT = "trackerscount" + + private val MIGRATE_1_2 = listOf( + "ALTER TABLE $TABLE_NAME ADD COLUMN $COLUMN_NAME_APPID TEXT" + // "ALTER TABLE $TABLE_NAME DROP COLUMN app_uid" + // DROP COLUMN is available since sqlite 3.35.0, and sdk29 as 3.22.0, sdk32 as 3.32.2 + ) } object AppTrackerEntry : BaseColumns { const val TABLE_NAME = "tracker_filter_stats" const val COLUMN_NAME_TIMESTAMP = "timestamp" const val COLUMN_NAME_TRACKER = "tracker" - const val COLUMN_NAME_APP_UID = "app_uid" const val COLUMN_NAME_NUMBER_CONTACTED = "sum_contacted" const val COLUMN_NAME_NUMBER_BLOCKED = "sum_blocked" + const val COLUMN_NAME_APPID = "app_apid" } private var projection = arrayOf( COLUMN_NAME_TIMESTAMP, - COLUMN_NAME_APP_UID, COLUMN_NAME_TRACKER, COLUMN_NAME_NUMBER_CONTACTED, - COLUMN_NAME_NUMBER_BLOCKED + COLUMN_NAME_NUMBER_BLOCKED, + COLUMN_NAME_APPID ) private val lock = Any() @@ -82,7 +91,13 @@ class StatsDatabase(context: Context) : } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - onCreate(db) + if (oldVersion == 1 && newVersion == 2) { + MIGRATE_1_2.forEach(db::execSQL) + } else { + Timber.e( + "Unexpected database versions: oldVersion: $oldVersion ; newVersion: $newVersion" + ) + } } private fun getCallsByPeriod( @@ -128,11 +143,11 @@ class StatsDatabase(context: Context) : javaPeriodFormat: String ): List<Pair<Int, Int>> { var currentDate = ZonedDateTime.now().minus(periodsCount.toLong(), periodUnit) - val formater = DateTimeFormatter.ofPattern(javaPeriodFormat) + val formatter = DateTimeFormatter.ofPattern(javaPeriodFormat) val calls = mutableListOf<Pair<Int, Int>>() for (i in 0 until periodsCount) { currentDate = currentDate.plus(1, periodUnit) - val currentPeriod = formater.format(currentDate) + val currentPeriod = formatter.format(currentDate) calls.add(callsByPeriod.getOrDefault(currentPeriod, 0 to 0)) } return calls @@ -182,15 +197,11 @@ class StatsDatabase(context: Context) : } } - fun getContactedTrackersCount(appUids: List<Int>?): Int { + fun getContactedTrackersCount(): Int { synchronized(lock) { val db = readableDatabase var query = "SELECT DISTINCT $COLUMN_NAME_TRACKER FROM $TABLE_NAME" - appUids?.let { - query += " WHERE $COLUMN_NAME_APP_UID IN (${it.joinToString(", ")})" - } - val cursor = db.rawQuery(query, arrayOf()) var count = 0 while (cursor.moveToNext()) { @@ -204,19 +215,19 @@ class StatsDatabase(context: Context) : } } - fun getContactedTrackersCountByApp(): Map<Int, Int> { + fun getContactedTrackersCountByAppId(): Map<String, Int> { synchronized(lock) { val db = readableDatabase - val projection = "$COLUMN_NAME_APP_UID, $COLUMN_NAME_TRACKER" + val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER" val cursor = db.rawQuery( "SELECT DISTINCT $projection FROM $TABLE_NAME", // + arrayOf() ) - val countByApp = mutableMapOf<Int, Int>() + val countByApp = mutableMapOf<String, Int>() while (cursor.moveToNext()) { trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { - val appUid = cursor.getInt(COLUMN_NAME_APP_UID) - countByApp[appUid] = countByApp.getOrDefault(appUid, 0) + 1 + val appId = cursor.getString(COLUMN_NAME_APPID) + countByApp[appId] = countByApp.getOrDefault(appId, 0) + 1 } } cursor.close() @@ -225,26 +236,26 @@ class StatsDatabase(context: Context) : } } - fun getCallsByApps(periodCount: Int, periodUnit: TemporalUnit): Map<Int, Pair<Int, Int>> { + fun getCallsByAppIds(periodCount: Int, periodUnit: TemporalUnit): Map<String, Pair<Int, Int>> { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase val selection = "$COLUMN_NAME_TIMESTAMP >= ?" val selectionArg = arrayOf("" + minTimestamp) - val projection = "$COLUMN_NAME_APP_UID, " + + val projection = "$COLUMN_NAME_APPID, " + "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," + "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM" val cursor = db.rawQuery( "SELECT $projection FROM $TABLE_NAME" + " WHERE $selection" + - " GROUP BY $COLUMN_NAME_APP_UID", + " GROUP BY $COLUMN_NAME_APPID", selectionArg ) - val callsByApp = HashMap<Int, Pair<Int, Int>>() + val callsByApp = HashMap<String, Pair<Int, Int>>() while (cursor.moveToNext()) { val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) - callsByApp[cursor.getInt(COLUMN_NAME_APP_UID)] = blocked to contacted - blocked + callsByApp[cursor.getString(COLUMN_NAME_APPID)] = blocked to contacted - blocked } cursor.close() db.close() @@ -252,13 +263,13 @@ class StatsDatabase(context: Context) : } } - fun getCalls(appUid: Int, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { + fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase - val selection = "$COLUMN_NAME_APP_UID = ? AND " + + val selection = "$COLUMN_NAME_APPID = ? AND " + "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + appUid, "" + minTimestamp) + val selectionArg = arrayOf("" + appId, "" + minTimestamp) val projection = "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," + "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM" @@ -278,37 +289,37 @@ class StatsDatabase(context: Context) : } } - fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): Int { + fun getMostLeakedAppId(periodCount: Int, periodUnit: TemporalUnit): String { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase val selection = "$COLUMN_NAME_TIMESTAMP >= ?" val selectionArg = arrayOf("" + minTimestamp) - val projection = "$COLUMN_NAME_APP_UID, " + + val projection = "$COLUMN_NAME_APPID, " + "SUM($COLUMN_NAME_NUMBER_CONTACTED - $COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_LEAKED_SUM" val cursor = db.rawQuery( "SELECT $projection FROM $TABLE_NAME" + " WHERE $selection" + - " GROUP BY $COLUMN_NAME_APP_UID" + + " GROUP BY $COLUMN_NAME_APPID" + " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1", selectionArg ) - var appUid = 0 + var appId = "" if (cursor.moveToNext()) { - appUid = cursor.getInt(COLUMN_NAME_APP_UID) + appId = cursor.getString(COLUMN_NAME_APPID) } cursor.close() db.close() - return appUid + return appId } } - fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { + fun logAccess(trackerId: String?, appId: String, blocked: Boolean) { synchronized(lock) { val currentHour = getCurrentHourTs() val db = writableDatabase val values = ContentValues() - values.put(COLUMN_NAME_APP_UID, appUid) + values.put(COLUMN_NAME_APPID, appId) values.put(COLUMN_NAME_TRACKER, trackerId) values.put(COLUMN_NAME_TIMESTAMP, currentHour) @@ -317,9 +328,9 @@ class StatsDatabase(context: Context) : query+=COLUMN_NAME_NUMBER_BLOCKED+" = "+COLUMN_NAME_NUMBER_BLOCKED+" + 1 "; */ val selection = "$COLUMN_NAME_TIMESTAMP = ? AND " + - "$COLUMN_NAME_APP_UID = ? AND " + + "$COLUMN_NAME_APPID = ? AND " + "$COLUMN_NAME_TRACKER = ? " - val selectionArg = arrayOf("" + currentHour, "" + appUid, trackerId) + val selectionArg = arrayOf("" + currentHour, "" + appId, trackerId) val cursor = db.query( TABLE_NAME, projection, @@ -355,23 +366,22 @@ class StatsDatabase(context: Context) : private fun cursorToEntry(cursor: Cursor): StatEntry { val entry = StatEntry() - entry.timestamp = - cursor.getLong(COLUMN_NAME_TIMESTAMP) - entry.app_uid = cursor.getInt(COLUMN_NAME_APP_UID) + entry.timestamp = cursor.getLong(COLUMN_NAME_TIMESTAMP) + entry.appId = cursor.getString(COLUMN_NAME_APPID) entry.sum_blocked = cursor.getInt(COLUMN_NAME_NUMBER_BLOCKED) entry.sum_contacted = cursor.getInt(COLUMN_NAME_NUMBER_CONTACTED) entry.tracker = cursor.getInt(COLUMN_NAME_TRACKER) return entry } - fun getTrackers(appUids: List<Int>?): List<Tracker> { + fun getTrackers(appIds: List<String>?): List<Tracker> { synchronized(lock) { - val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APP_UID) + val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID) var selection: String? = null var selectionArg: Array<String>? = null - appUids?.let { - selection = "$COLUMN_NAME_APP_UID IN (${it.joinToString(", ")})" + appIds?.let { appIds -> + selection = "$COLUMN_NAME_APPID IN (${appIds.joinToString(", ") { "'$it'" }})" selectionArg = arrayOf() } @@ -402,7 +412,7 @@ class StatsDatabase(context: Context) : } class StatEntry { - var app_uid = 0 + var appId = "" var sum_contacted = 0 var sum_blocked = 0 var timestamp: Long = 0 @@ -442,6 +452,8 @@ class StatsDatabase(context: Context) : private fun Cursor.getString(columnName: String): String { val columnIndex = getColumnIndex(columnName) - return if (columnIndex >= 0) getString(columnIndex) else "" + return if (columnIndex >= 0) { + getStringOrNull(columnIndex) ?: "" + } else "" } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt index 16d8ec6..8f02adb 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -18,12 +19,15 @@ package foundation.e.privacymodules.trackers.data import android.content.Context +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.api.Tracker import java.time.temporal.TemporalUnit class StatsRepository private constructor(context: Context) { private val database: StatsDatabase private var newDataCallback: (() -> Unit)? = null + private var getAppByUid: ((Int) -> ApplicationDescription?)? = null + private var getAppByAPId: ((String) -> ApplicationDescription?)? = null companion object { private var instance: StatsRepository? = null @@ -32,6 +36,14 @@ class StatsRepository private constructor(context: Context) { } } + fun setAppGetters( + getAppByUid: (Int) -> ApplicationDescription?, + getAppByAPId: (String) -> ApplicationDescription? + ) { + this.getAppByUid = getAppByUid + this.getAppByAPId = getAppByAPId + } + init { database = StatsDatabase(context) } @@ -41,8 +53,10 @@ class StatsRepository private constructor(context: Context) { } fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { - database.logAccess(trackerId, appUid, blocked) - newDataCallback?.invoke() + getAppByUid?.invoke(appUid)?.let { app -> + database.logAccess(trackerId, app.apId, blocked) + newDataCallback?.invoke() + } } fun getTrackersCallsOnPeriod( @@ -56,27 +70,36 @@ class StatsRepository private constructor(context: Context) { return database.getActiveTrackersByPeriod(periodsCount, periodUnit) } - fun getContactedTrackersCountByApp(): Map<Int, Int> { - return database.getContactedTrackersCountByApp() + fun getContactedTrackersCountByApp(): Map<ApplicationDescription, Int> { + return database.getContactedTrackersCountByAppId().mapByAppIdToApp() } - fun getContactedTrackersCount(appUids: List<Int>?): Int { - return database.getContactedTrackersCount(appUids) + fun getContactedTrackersCount(): Int { + return database.getContactedTrackersCount() } - fun getTrackers(appUids: List<Int>?): List<Tracker> { - return database.getTrackers(appUids) + fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> { + return database.getTrackers(apps?.map { it.apId }) + } + + fun getCallsByApps( + periodCount: Int, + periodUnit: TemporalUnit + ): Map<ApplicationDescription, Pair<Int, Int>> { + return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp() } - fun getCallsByApps(periodCount: Int, periodUnit: TemporalUnit): Map<Int, Pair<Int, Int>> { - return database.getCallsByApps(periodCount, periodUnit) + fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { + return database.getCalls(app.apId, periodCount, periodUnit) } - fun getCalls(appUid: Int, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { - return database.getCalls(appUid, periodCount, periodUnit) + fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): ApplicationDescription? { + return getAppByAPId?.invoke(database.getMostLeakedAppId(periodCount, periodUnit)) } - fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): Int { - return database.getMostLeakedApp(periodCount, periodUnit) + private fun <K> Map<String, K>.mapByAppIdToApp(): Map<ApplicationDescription, K> { + return entries.mapNotNull { (apId, value) -> + getAppByAPId?.invoke(apId)?.let { it to value } + }.toMap() } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt index e9f049d..2763d06 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -19,18 +20,28 @@ package foundation.e.privacymodules.trackers.data import android.content.Context import android.content.SharedPreferences +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.api.Tracker +import java.io.File class WhitelistRepository private constructor(context: Context) { - private lateinit var appsWhitelist: Set<Int> - private val trackersWhitelistByApp: MutableMap<Int, MutableSet<String>> = HashMap() + private var appsWhitelist: Set<String> = HashSet() + private var appUidsWhitelist: Set<Int> = HashSet() + + private var trackersWhitelistByApp: MutableMap<String, MutableSet<String>> = HashMap() + private var trackersWhitelistByUid: Map<Int, MutableSet<String>> = HashMap() + private val prefs: SharedPreferences + private var getAppByAPId: ((String) -> ApplicationDescription?)? = null companion object { - private const val SHARED_PREFS_FILE = "trackers_whitelist.prefs" - private const val KEY_BLOKING_ENABLED = "blocking_enabled" + private const val SHARED_PREFS_FILE = "trackers_whitelist_v2" + private const val KEY_BLOCKING_ENABLED = "blocking_enabled" private const val KEY_APPS_WHITELIST = "apps_whitelist" private const val KEY_APP_TRACKERS_WHITELIST_PREFIX = "app_trackers_whitelist_" + + private const val SHARED_PREFS_FILE_V1 = "trackers_whitelist.prefs" + private var instance: WhitelistRepository? = null fun getInstance(context: Context): WhitelistRepository { return instance ?: WhitelistRepository(context).apply { instance = this } @@ -42,83 +53,155 @@ class WhitelistRepository private constructor(context: Context) { reloadCache() } + fun setAppGetters( + context: Context, + getAppByAPId: (String) -> ApplicationDescription?, + getAppByUid: (Int) -> ApplicationDescription? + ) { + this.getAppByAPId = getAppByAPId + migrate(context, getAppByUid) + } + + private fun migrate(context: Context, getAppByUid: (Int) -> ApplicationDescription?) { + if (context.sharedPreferencesExists(SHARED_PREFS_FILE_V1)) { + migrate1To2(context, getAppByUid) + } + } + + private fun Context.sharedPreferencesExists(fileName: String): Boolean { + return File( + "${applicationInfo.dataDir}/shared_prefs/$fileName.xml" + ).exists() + } + + private fun migrate1To2(context: Context, getAppByUid: (Int) -> ApplicationDescription?) { + val prefsV1 = context.getSharedPreferences(SHARED_PREFS_FILE_V1, Context.MODE_PRIVATE) + val editorV2 = prefs.edit() + + editorV2.putBoolean(KEY_BLOCKING_ENABLED, prefsV1.getBoolean(KEY_BLOCKING_ENABLED, false)) + + val apIds = prefsV1.getStringSet(KEY_APPS_WHITELIST, HashSet())?.mapNotNull { + try { + val uid = it.toInt() + getAppByUid(uid)?.apId + } catch (e: Exception) { null } + }?.toSet() ?: HashSet() + + editorV2.putStringSet(KEY_APPS_WHITELIST, apIds) + + prefsV1.all.keys.forEach { key -> + if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) { + try { + val uid = key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length).toInt() + val apId = getAppByUid(uid)?.apId + apId?.let { + val trackers = prefsV1.getStringSet(key, emptySet()) + editorV2.putStringSet(buildAppTrackersKey(apId), trackers) + } + } catch (e: Exception) { } + } + } + editorV2.commit() + + context.deleteSharedPreferences(SHARED_PREFS_FILE_V1) + + reloadCache() + } + private fun reloadCache() { - isBlockingEnabled = prefs.getBoolean(KEY_BLOKING_ENABLED, false) + isBlockingEnabled = prefs.getBoolean(KEY_BLOCKING_ENABLED, false) reloadAppsWhiteList() reloadAllAppTrackersWhiteList() } private fun reloadAppsWhiteList() { - appsWhitelist = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet())?.mapNotNull { - try { it.toInt() } catch (e: Exception) { null } - }?.toHashSet() ?: HashSet() + appsWhitelist = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet()) ?: HashSet() + appUidsWhitelist = appsWhitelist + .mapNotNull { apId -> getAppByAPId?.invoke(apId)?.uid } + .toSet() } - private fun reloadAppTrackersWhiteList(appUid: Int) { - val key = buildAppTrackersKey(appUid) - trackersWhitelistByApp[appUid] = prefs.getStringSet(key, HashSet()) ?: HashSet() + private fun refreshAppUidTrackersWhiteList() { + trackersWhitelistByUid = trackersWhitelistByApp.mapNotNull { (apId, value) -> + getAppByAPId?.invoke(apId)?.uid?.let { uid -> + uid to value + } + }.toMap() } - private fun reloadAllAppTrackersWhiteList() { - trackersWhitelistByApp.clear() + val map: MutableMap<String, MutableSet<String>> = HashMap() prefs.all.keys.forEach { key -> if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) { - val appUid = key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length).toInt() - reloadAppTrackersWhiteList(appUid) + map[key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length)] = ( + prefs.getStringSet(key, HashSet()) ?: HashSet() + ) } } + trackersWhitelistByApp = map } var isBlockingEnabled: Boolean = false get() = field set(enabled) { - prefs.edit().putBoolean(KEY_BLOKING_ENABLED, enabled).apply() + prefs.edit().putBoolean(KEY_BLOCKING_ENABLED, enabled).apply() field = enabled } - fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) { + fun setWhiteListed(apId: String, isWhiteListed: Boolean) { val current = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet())?.toHashSet() ?: HashSet() if (isWhiteListed) { - current.add("" + appUid) + current.add(apId) } else { - current.remove("" + appUid) + current.remove(apId) } prefs.edit().putStringSet(KEY_APPS_WHITELIST, current).commit() reloadAppsWhiteList() } - private fun buildAppTrackersKey(appUid: Int): String { - return KEY_APP_TRACKERS_WHITELIST_PREFIX + appUid + private fun buildAppTrackersKey(apId: String): String { + return KEY_APP_TRACKERS_WHITELIST_PREFIX + apId } - fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) { - val trackers = trackersWhitelistByApp.getOrDefault(appUid, HashSet()) - trackersWhitelistByApp[appUid] = trackers + fun setWhiteListed(tracker: Tracker, apId: String, isWhiteListed: Boolean) { + val trackers = trackersWhitelistByApp.getOrDefault(apId, HashSet()) + trackersWhitelistByApp[apId] = trackers if (isWhiteListed) { trackers.add(tracker.id) } else { trackers.remove(tracker.id) } - prefs.edit().putStringSet(buildAppTrackersKey(appUid), trackers).commit() + refreshAppUidTrackersWhiteList() + prefs.edit().putStringSet(buildAppTrackersKey(apId), trackers).commit() } - fun isAppWhiteListed(appUid: Int): Boolean { - return appsWhitelist.contains(appUid) + fun isAppWhiteListed(app: ApplicationDescription): Boolean { + return appsWhitelist.contains(app.apId) } - fun isTrackerWhiteListedForApp(trackerId: String?, appUid: Int): Boolean { - return trackersWhitelistByApp.getOrDefault(appUid, HashSet()).contains(trackerId) + fun isWhiteListed(appUid: Int, trackerId: String?): Boolean { + return appUidsWhitelist.contains(appUid) || + trackersWhitelistByUid.getOrDefault(appUid, HashSet()).contains(trackerId) } fun areWhiteListEmpty(): Boolean { return appsWhitelist.isEmpty() && trackersWhitelistByApp.all { (_, trackers) -> trackers.isEmpty() } } - val whiteListedApp: List<Int> get() = appsWhitelist.toList() + fun getWhiteListedApp(): List<ApplicationDescription> { + return getAppByAPId?.let { + appsWhitelist.mapNotNull(it) + } ?: emptyList() + } + + fun getWhiteListForApp(app: ApplicationDescription): List<String> { + return trackersWhitelistByApp[app.apId]?.toList() ?: emptyList() + } - fun getWhiteListForApp(appUid: Int): List<String> { - return trackersWhitelistByApp[appUid]?.toList() ?: emptyList() + fun clearWhiteList(apId: String) { + trackersWhitelistByApp.remove(apId) + refreshAppUidTrackersWhiteList() + prefs.edit().remove(buildAppTrackersKey(apId)).commit() } } |