From 53f4a9ce311d612d43fa770cf7e8f8e98fbb43a0 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Tue, 12 Sep 2023 06:17:39 +0000 Subject: 2: organise module with clean archi, use Koin for injection. --- trackers/build.gradle | 9 +- trackers/src/main/AndroidManifest.xml | 5 +- .../e/advancedprivacy/trackers/KoinModule.kt | 72 ++++ .../trackers/data/ETrackersResponse.kt | 10 + .../trackers/data/RemoteTrackersListRepository.kt | 61 +++ .../advancedprivacy/trackers/data/StatsDatabase.kt | 461 +++++++++++++++++++++ .../trackers/data/TrackersRepository.kt | 109 +++++ .../trackers/data/WhitelistRepository.kt | 195 +++++++++ .../trackers/domain/entities/Tracker.kt | 28 ++ .../trackers/domain/usecases/DNSBlocker.kt | 143 +++++++ .../trackers/domain/usecases/StatisticsUseCase.kt | 86 ++++ .../trackers/domain/usecases/TrackersLogger.kt | 60 +++ .../domain/usecases/UpdateTrackerListUseCase.kt | 29 ++ .../trackers/services/DNSBlockerService.kt | 68 +++ .../trackers/services/ForegroundStarter.kt | 45 ++ .../trackers/services/UpdateTrackersWorker.kt | 60 +++ .../privacymodules/trackers/DNSBlockerRunnable.kt | 141 ------- .../e/privacymodules/trackers/DNSBlockerService.kt | 79 ---- .../e/privacymodules/trackers/ForegroundStarter.kt | 45 -- .../e/privacymodules/trackers/TrackersLogger.kt | 69 --- .../trackers/api/BlockTrackersPrivacyModule.kt | 98 ----- .../trackers/api/IBlockTrackersPrivacyModule.kt | 98 ----- .../trackers/api/ITrackTrackersPrivacyModule.kt | 110 ----- .../trackers/api/TrackTrackersPrivacyModule.kt | 126 ------ .../e/privacymodules/trackers/api/Tracker.kt | 28 -- .../privacymodules/trackers/data/StatsDatabase.kt | 459 -------------------- .../trackers/data/StatsRepository.kt | 105 ----- .../trackers/data/TrackersRepository.kt | 57 --- .../trackers/data/WhitelistRepository.kt | 207 --------- 29 files changed, 1438 insertions(+), 1625 deletions(-) create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/ETrackersResponse.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/entities/Tracker.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/ForegroundStarter.kt create mode 100644 trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/UpdateTrackersWorker.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerService.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/ForegroundStarter.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/api/Tracker.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/data/TrackersRepository.kt delete mode 100644 trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt (limited to 'trackers') diff --git a/trackers/build.gradle b/trackers/build.gradle index bb9489a..737db5a 100644 --- a/trackers/build.gradle +++ b/trackers/build.gradle @@ -1,4 +1,5 @@ /* + Copyright (C) 2023 MURENA SAS Copyright (C) 2022 ECORP This program is free software; you can redistribute it and/or @@ -42,9 +43,15 @@ android { } dependencies { - implementation project(':privacymodule-api') + implementation project(':core') implementation( + libs.androidx.work.ktx, + libs.bundles.koin, libs.bundles.kotlin.android.coroutines, + libs.google.gson, + libs.retrofit, + libs.retrofit.scalars, + libs.timber ) } diff --git a/trackers/src/main/AndroidManifest.xml b/trackers/src/main/AndroidManifest.xml index debdf61..615d310 100644 --- a/trackers/src/main/AndroidManifest.xml +++ b/trackers/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + package="foundation.e.advancedprivacy.trackers"> @@ -29,7 +30,7 @@ diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt new file mode 100644 index 0000000..0cfb69c --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers + +import foundation.e.advancedprivacy.data.repositories.RemoteTrackersListRepository +import foundation.e.advancedprivacy.trackers.data.StatsDatabase +import foundation.e.advancedprivacy.trackers.data.TrackersRepository +import foundation.e.advancedprivacy.trackers.data.WhitelistRepository +import foundation.e.advancedprivacy.trackers.domain.usecases.DNSBlocker +import foundation.e.advancedprivacy.trackers.domain.usecases.StatisticsUseCase +import foundation.e.advancedprivacy.trackers.domain.usecases.TrackersLogger +import foundation.e.advancedprivacy.trackers.domain.usecases.UpdateTrackerListUseCase +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +val trackersModule = module { + + factoryOf(::RemoteTrackersListRepository) + factoryOf(::UpdateTrackerListUseCase) + + singleOf(::TrackersRepository) + single { + StatsDatabase( + context = androidContext(), + trackersRepository = get() + ) + } + + single { + StatisticsUseCase( + database = get(), + appListsRepository = get() + ) + } + + single { + WhitelistRepository( + context = androidContext(), + appListsRepository = get() + ) + } + + factory { + DNSBlocker( + context = androidContext(), + trackersLogger = get(), + trackersRepository = get(), + whitelistRepository = get() + ) + } + + factory { + TrackersLogger(statisticsUseCase = get()) + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/ETrackersResponse.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/ETrackersResponse.kt new file mode 100644 index 0000000..1b38ecf --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/ETrackersResponse.kt @@ -0,0 +1,10 @@ +package foundation.e.advancedprivacy.trackers.data + +data class ETrackersResponse(val trackers: List) { + data class ETracker( + val id: String?, + val hostnames: List?, + val name: String?, + val exodusId: String? + ) +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt new file mode 100644 index 0000000..c2c0768 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.data.repositories + +import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.http.GET +import timber.log.Timber +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.io.PrintWriter + +class RemoteTrackersListRepository { + + fun saveData(file: File, data: String): Boolean { + try { + val fos = FileWriter(file, false) + val ps = PrintWriter(fos) + ps.apply { + print(data) + flush() + close() + } + return true + } catch (e: IOException) { + Timber.e("While saving tracker file.", e) + } + return false + } +} + +interface ETrackersApi { + companion object { + fun build(): ETrackersApi { + val retrofit = Retrofit.Builder() + .baseUrl("https://gitlab.e.foundation/e/os/tracker-list/-/raw/main/") + .addConverterFactory(ScalarsConverterFactory.create()) + .build() + return retrofit.create(ETrackersApi::class.java) + } + } + + @GET("list/e_trackers.json") + suspend fun trackers(): String +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt new file mode 100644 index 0000000..6aa76cf --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.data + +import android.content.ContentValues +import android.content.Context +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.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APPID +import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_BLOCKED +import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_CONTACTED +import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TIMESTAMP +import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TRACKER +import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.TABLE_NAME +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import timber.log.Timber +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import java.time.temporal.TemporalUnit +import java.util.concurrent.TimeUnit + +class StatsDatabase( + context: Context, + private val trackersRepository: TrackersRepository +) : + SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + companion object { + 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_TRACKER TEXT," + + "$COLUMN_NAME_NUMBER_CONTACTED 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_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_TRACKER, + COLUMN_NAME_NUMBER_CONTACTED, + COLUMN_NAME_NUMBER_BLOCKED, + COLUMN_NAME_APPID + ) + + private val lock = Any() + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(SQL_CREATE_TABLE) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + if (oldVersion == 1 && newVersion == 2) { + MIGRATE_1_2.forEach(db::execSQL) + } else { + Timber.e( + "Unexpected database versions: oldVersion: $oldVersion ; newVersion: $newVersion" + ) + } + } + + private fun getCallsByPeriod( + periodsCount: Int, + periodUnit: TemporalUnit, + sqlitePeriodFormat: String + ): Map> { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodsCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + minTimestamp) + + val projection = ( + "$COLUMN_NAME_TIMESTAMP, " + + "STRFTIME('$sqlitePeriodFormat', DATETIME($COLUMN_NAME_TIMESTAMP, 'unixepoch', 'localtime')) $PROJECTION_NAME_PERIOD," + + "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 $PROJECTION_NAME_PERIOD" + + " ORDER BY $COLUMN_NAME_TIMESTAMP DESC LIMIT $periodsCount", + selectionArg + ) + val callsByPeriod = HashMap>() + while (cursor.moveToNext()) { + val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) + val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) + callsByPeriod[cursor.getString(PROJECTION_NAME_PERIOD)] = blocked to contacted - blocked + } + cursor.close() + db.close() + return callsByPeriod + } + } + + private fun callsByPeriodToPeriodsList( + callsByPeriod: Map>, + periodsCount: Int, + periodUnit: TemporalUnit, + javaPeriodFormat: String + ): List> { + var currentDate = ZonedDateTime.now().minus(periodsCount.toLong(), periodUnit) + val formatter = DateTimeFormatter.ofPattern(javaPeriodFormat) + val calls = mutableListOf>() + for (i in 0 until periodsCount) { + currentDate = currentDate.plus(1, periodUnit) + val currentPeriod = formatter.format(currentDate) + calls.add(callsByPeriod.getOrDefault(currentPeriod, 0 to 0)) + } + return calls + } + + fun getTrackersCallsOnPeriod( + periodsCount: Int, + periodUnit: TemporalUnit + ): List> { + var sqlitePeriodFormat = "%Y%m" + var javaPeriodFormat = "yyyyMM" + if (periodUnit === ChronoUnit.MONTHS) { + sqlitePeriodFormat = "%Y%m" + javaPeriodFormat = "yyyyMM" + } else if (periodUnit === ChronoUnit.DAYS) { + sqlitePeriodFormat = "%Y%m%d" + javaPeriodFormat = "yyyyMMdd" + } else if (periodUnit === ChronoUnit.HOURS) { + sqlitePeriodFormat = "%Y%m%d%H" + javaPeriodFormat = "yyyyMMddHH" + } + val callsByPeriod = getCallsByPeriod(periodsCount, periodUnit, sqlitePeriodFormat) + return callsByPeriodToPeriodsList(callsByPeriod, periodsCount, periodUnit, javaPeriodFormat) + } + + fun getActiveTrackersByPeriod(periodsCount: Int, periodUnit: TemporalUnit): Int { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodsCount, periodUnit) + val db = writableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ? AND " + + "$COLUMN_NAME_NUMBER_CONTACTED > $COLUMN_NAME_NUMBER_BLOCKED" + val selectionArg = arrayOf("" + minTimestamp) + val projection = + "COUNT(DISTINCT $COLUMN_NAME_TRACKER) $PROJECTION_NAME_TRACKERS_COUNT" + + val cursor = db.rawQuery( + "SELECT $projection FROM $TABLE_NAME WHERE $selection", + selectionArg + ) + var count = 0 + if (cursor.moveToNext()) { + count = cursor.getInt(0) + } + cursor.close() + db.close() + return count + } + } + + fun getContactedTrackersCount(): Int { + synchronized(lock) { + val db = readableDatabase + var query = "SELECT DISTINCT $COLUMN_NAME_TRACKER FROM $TABLE_NAME" + + val cursor = db.rawQuery(query, arrayOf()) + var count = 0 + while (cursor.moveToNext()) { + trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { + count++ + } + } + cursor.close() + db.close() + return count + } + } + + fun getContactedTrackersCountByAppId(): Map { + synchronized(lock) { + val db = readableDatabase + val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER" + val cursor = db.rawQuery( + "SELECT DISTINCT $projection FROM $TABLE_NAME", // + + arrayOf() + ) + val countByApp = mutableMapOf() + while (cursor.moveToNext()) { + trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { + val appId = cursor.getString(COLUMN_NAME_APPID) + countByApp[appId] = countByApp.getOrDefault(appId, 0) + 1 + } + } + cursor.close() + db.close() + return countByApp + } + } + + fun getCallsByAppIds(periodCount: Int, periodUnit: TemporalUnit): Map> { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + minTimestamp) + 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_APPID", + selectionArg + ) + val callsByApp = HashMap>() + while (cursor.moveToNext()) { + val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) + val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) + callsByApp[cursor.getString(COLUMN_NAME_APPID)] = blocked to contacted - blocked + } + cursor.close() + db.close() + return callsByApp + } + } + + fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_APPID = ? AND " + + "$COLUMN_NAME_TIMESTAMP >= ?" + 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" + val cursor = db.rawQuery( + "SELECT $projection FROM $TABLE_NAME WHERE $selection", + selectionArg + ) + var calls: Pair = 0 to 0 + if (cursor.moveToNext()) { + val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) + val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) + calls = blocked to contacted - blocked + } + cursor.close() + db.close() + return calls + } + } + + 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_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_APPID" + + " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1", + selectionArg + ) + var appId = "" + if (cursor.moveToNext()) { + appId = cursor.getString(COLUMN_NAME_APPID) + } + cursor.close() + db.close() + return appId + } + } + + fun logAccess(trackerId: String?, appId: String, blocked: Boolean) { + synchronized(lock) { + val currentHour = getCurrentHourTs() + val db = writableDatabase + val values = ContentValues() + values.put(COLUMN_NAME_APPID, appId) + values.put(COLUMN_NAME_TRACKER, trackerId) + values.put(COLUMN_NAME_TIMESTAMP, currentHour) + + /*String query = "UPDATE product SET "+COLUMN_NAME_NUMBER_CONTACTED+" = "+COLUMN_NAME_NUMBER_CONTACTED+" + 1 "; + if(blocked) + query+=COLUMN_NAME_NUMBER_BLOCKED+" = "+COLUMN_NAME_NUMBER_BLOCKED+" + 1 "; +*/ + val selection = "$COLUMN_NAME_TIMESTAMP = ? AND " + + "$COLUMN_NAME_APPID = ? AND " + + "$COLUMN_NAME_TRACKER = ? " + val selectionArg = arrayOf("" + currentHour, "" + appId, trackerId) + val cursor = db.query( + TABLE_NAME, + projection, + selection, + selectionArg, + null, + null, + null + ) + if (cursor.count > 0) { + cursor.moveToFirst() + val entry = cursorToEntry(cursor) + if (blocked) values.put( + COLUMN_NAME_NUMBER_BLOCKED, + entry.sum_blocked + 1 + ) else values.put(COLUMN_NAME_NUMBER_BLOCKED, entry.sum_blocked) + values.put(COLUMN_NAME_NUMBER_CONTACTED, entry.sum_contacted + 1) + db.update(TABLE_NAME, values, selection, selectionArg) + + // db.execSQL(query, new String[]{""+hour, ""+day, ""+month, ""+year, ""+appUid, ""+trackerId}); + } else { + if (blocked) values.put( + COLUMN_NAME_NUMBER_BLOCKED, + 1 + ) else values.put(COLUMN_NAME_NUMBER_BLOCKED, 0) + values.put(COLUMN_NAME_NUMBER_CONTACTED, 1) + db.insert(TABLE_NAME, null, values) + } + cursor.close() + db.close() + } + } + + private fun cursorToEntry(cursor: Cursor): StatEntry { + val entry = StatEntry() + 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(appIds: List?): List { + synchronized(lock) { + val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID) + var selection: String? = null + + var selectionArg: Array? = null + appIds?.let { appIds -> + selection = "$COLUMN_NAME_APPID IN (${appIds.joinToString(", ") { "'$it'" }})" + selectionArg = arrayOf() + } + + val db = readableDatabase + val cursor = db.query( + true, + TABLE_NAME, + columns, + selection, + selectionArg, + null, + null, + null, + null + ) + val trackers: MutableList = ArrayList() + while (cursor.moveToNext()) { + val trackerId = cursor.getString(COLUMN_NAME_TRACKER) + val tracker = trackersRepository.getTracker(trackerId) + if (tracker != null) { + trackers.add(tracker) + } + } + cursor.close() + db.close() + return trackers + } + } + + class StatEntry { + var appId = "" + var sum_contacted = 0 + var sum_blocked = 0 + var timestamp: Long = 0 + var tracker = 0 + } + + private fun getCurrentHourTs(): Long { + val hourInMs = TimeUnit.HOURS.toMillis(1L) + val hourInS = TimeUnit.HOURS.toSeconds(1L) + return System.currentTimeMillis() / hourInMs * hourInS + } + + private fun getPeriodStartTs( + periodsCount: Int, + periodUnit: TemporalUnit + ): Long { + var start = ZonedDateTime.now() + .minus(periodsCount.toLong(), periodUnit) + .plus(1, periodUnit) + var truncatePeriodUnit = periodUnit + if (periodUnit === ChronoUnit.MONTHS) { + start = start.withDayOfMonth(1) + truncatePeriodUnit = ChronoUnit.DAYS + } + return start.truncatedTo(truncatePeriodUnit).toEpochSecond() + } + + private fun Cursor.getInt(columnName: String): Int { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex >= 0) getInt(columnIndex) else 0 + } + + private fun Cursor.getLong(columnName: String): Long { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex >= 0) getLong(columnIndex) else 0 + } + + private fun Cursor.getString(columnName: String): String { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex >= 0) { + getStringOrNull(columnIndex) ?: "" + } else "" + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt new file mode 100644 index 0000000..a7d5e49 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.data + +import android.content.Context +import com.google.gson.Gson +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader + +class TrackersRepository( + private val context: Context, + coroutineScope: CoroutineScope +) { + + private var trackersById: Map = HashMap() + private var hostnameToId: Map = HashMap() + + private val eTrackerFileName = "e_trackers.json" + val eTrackerFile = File(context.filesDir.absolutePath, eTrackerFileName) + + init { + coroutineScope.launch(Dispatchers.IO) { + initTrackersFile() + } + } + fun initTrackersFile() { + try { + var inputStream = context.assets.open(eTrackerFileName) + if (eTrackerFile.exists()) { + inputStream = FileInputStream(eTrackerFile) + } + val reader = InputStreamReader(inputStream, "UTF-8") + val trackerResponse = + Gson().fromJson(reader, ETrackersResponse::class.java) + + setTrackersList(mapper(trackerResponse)) + + reader.close() + inputStream.close() + } catch (e: Exception) { + Timber.e("While parsing trackers in assets", e) + } + } + + private fun mapper(response: ETrackersResponse): List { + return response.trackers.mapNotNull { + try { + it.toTracker() + } catch (e: Exception) { + null + } + } + } + + private fun ETrackersResponse.ETracker.toTracker(): Tracker { + return Tracker( + id = id!!, + hostnames = hostnames!!.toSet(), + label = name!!, + exodusId = exodusId + ) + } + + private fun setTrackersList(list: List) { + val trackersById: MutableMap = HashMap() + val hostnameToId: MutableMap = HashMap() + list.forEach { tracker -> + trackersById[tracker.id] = tracker + for (hostname in tracker.hostnames) { + hostnameToId[hostname] = tracker.id + } + } + this.trackersById = trackersById + this.hostnameToId = hostnameToId + } + + fun isTracker(hostname: String?): Boolean { + return hostnameToId.containsKey(hostname) + } + + fun getTrackerId(hostname: String?): String? { + return hostnameToId[hostname] + } + + fun getTracker(id: String?): Tracker? { + return trackersById[id] + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt new file mode 100644 index 0000000..429c5e9 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.data + +import android.content.Context +import android.content.SharedPreferences +import foundation.e.advancedprivacy.data.repositories.AppListsRepository +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import java.io.File + +class WhitelistRepository( + context: Context, + private val appListsRepository: AppListsRepository +) { + private var appsWhitelist: Set = HashSet() + private var appUidsWhitelist: Set = HashSet() + + private var trackersWhitelistByApp: MutableMap> = HashMap() + private var trackersWhitelistByUid: Map> = HashMap() + + private val prefs: SharedPreferences + + companion object { + 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" + } + + init { + prefs = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) + reloadCache() + migrate(context) + } + + private fun migrate(context: Context) { + if (context.sharedPreferencesExists(SHARED_PREFS_FILE_V1)) { + migrate1To2(context) + } + } + + private fun Context.sharedPreferencesExists(fileName: String): Boolean { + return File( + "${applicationInfo.dataDir}/shared_prefs/$fileName.xml" + ).exists() + } + + private fun migrate1To2(context: Context) { + 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() + appListsRepository.getApp(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 = appListsRepository.getApp(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_BLOCKING_ENABLED, false) + reloadAppsWhiteList() + reloadAllAppTrackersWhiteList() + } + + private fun reloadAppsWhiteList() { + appsWhitelist = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet()) ?: HashSet() + appUidsWhitelist = appsWhitelist + .mapNotNull { apId -> appListsRepository.getApp(apId)?.uid } + .toSet() + } + + private fun refreshAppUidTrackersWhiteList() { + trackersWhitelistByUid = trackersWhitelistByApp.mapNotNull { (apId, value) -> + appListsRepository.getApp(apId)?.uid?.let { uid -> + uid to value + } + }.toMap() + } + private fun reloadAllAppTrackersWhiteList() { + val map: MutableMap> = HashMap() + prefs.all.keys.forEach { key -> + if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) { + 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_BLOCKING_ENABLED, enabled).apply() + field = enabled + } + + fun setWhiteListed(apId: String, isWhiteListed: Boolean) { + val current = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet())?.toHashSet() ?: HashSet() + + if (isWhiteListed) { + current.add(apId) + } else { + current.remove(apId) + } + prefs.edit().putStringSet(KEY_APPS_WHITELIST, current).commit() + reloadAppsWhiteList() + } + + private fun buildAppTrackersKey(apId: String): String { + return KEY_APP_TRACKERS_WHITELIST_PREFIX + apId + } + + 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) + } + refreshAppUidTrackersWhiteList() + prefs.edit().putStringSet(buildAppTrackersKey(apId), trackers).commit() + } + + fun isAppWhiteListed(app: ApplicationDescription): Boolean { + return appsWhitelist.contains(app.apId) + } + + 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() } + } + + fun getWhiteListedApp(): List { + return appsWhitelist.mapNotNull(appListsRepository::getApp) + } + + fun getWhiteListForApp(app: ApplicationDescription): List { + return trackersWhitelistByApp[app.apId]?.toList() ?: emptyList() + } + + fun clearWhiteList(apId: String) { + trackersWhitelistByApp.remove(apId) + refreshAppUidTrackersWhiteList() + prefs.edit().remove(buildAppTrackersKey(apId)).commit() + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/entities/Tracker.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/entities/Tracker.kt new file mode 100644 index 0000000..5c31294 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/entities/Tracker.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.domain.entities + +/** + * Describe a tracker. + */ +data class Tracker( + val id: String, + val hostnames: Set, + val label: String, + val exodusId: String? +) diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt new file mode 100644 index 0000000..fb08910 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.domain.usecases + +import android.content.Context +import android.content.pm.PackageManager +import android.net.LocalServerSocket +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import foundation.e.advancedprivacy.trackers.data.TrackersRepository +import foundation.e.advancedprivacy.trackers.data.WhitelistRepository +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.BufferedReader +import java.io.InputStreamReader +import java.io.PrintWriter + +class DNSBlocker( + context: Context, + val trackersLogger: TrackersLogger, + private val trackersRepository: TrackersRepository, + private val whitelistRepository: WhitelistRepository +) { + private var resolverReceiver: LocalServerSocket? = null + private var eBrowserAppUid = -1 + + companion object { + private const val SOCKET_NAME = "foundation.e.advancedprivacy" + private const val E_BROWSER_DOT_SERVER = "chrome.cloudflare-dns.com" + } + + init { + initEBrowserDoTFix(context) + } + + private fun closeSocket() { + // Known bug and workaround that LocalServerSocket::close is not working well + // https://issuetracker.google.com/issues/36945762 + if (resolverReceiver != null) { + try { + Os.shutdown(resolverReceiver!!.fileDescriptor, OsConstants.SHUT_RDWR) + resolverReceiver!!.close() + resolverReceiver = null + } catch (e: ErrnoException) { + if (e.errno != OsConstants.EBADF) { + Timber.w("Socket already closed") + } else { + Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") + } + } catch (e: Exception) { + Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") + } + } + } + + fun listenJob(scope: CoroutineScope): Job = scope.launch(Dispatchers.IO) { + val resolverReceiver = runSuspendCatching { + LocalServerSocket(SOCKET_NAME) + }.getOrElse { + Timber.e(it, "Exception: cannot open DNS port on $SOCKET_NAME") + return@launch + } + + this@DNSBlocker.resolverReceiver = resolverReceiver + Timber.d("DNSFilterProxy running on port $SOCKET_NAME") + + while (isActive) { + runSuspendCatching { + val socket = resolverReceiver.accept() + val reader = BufferedReader(InputStreamReader(socket.inputStream)) + val line = reader.readLine() + val params = line.split(",").toTypedArray() + val output = socket.outputStream + val writer = PrintWriter(output, true) + val domainName = params[0] + val appUid = params[1].toInt() + var isBlocked = false + if (isEBrowserDoTBlockFix(appUid, domainName)) { + isBlocked = true + } else if (trackersRepository.isTracker(domainName)) { + val trackerId = trackersRepository.getTrackerId(domainName) + if (shouldBlock(appUid, trackerId)) { + writer.println("block") + isBlocked = true + } + trackersLogger.logAccess(trackerId, appUid, isBlocked) + } + if (!isBlocked) { + writer.println("pass") + } + socket.close() + }.onFailure { + if (it is CancellationException) { + closeSocket() + throw it + } else { + Timber.w(it, "Exception while listening DNS resolver") + } + } + } + } + + private fun initEBrowserDoTFix(context: Context) { + try { + eBrowserAppUid = + context.packageManager.getApplicationInfo("foundation.e.browser", 0).uid + } catch (e: PackageManager.NameNotFoundException) { + Timber.i(e, "no E Browser package found.") + } + } + + private fun isEBrowserDoTBlockFix(appUid: Int, hostname: String): Boolean { + return appUid == eBrowserAppUid && E_BROWSER_DOT_SERVER == hostname + } + + private fun shouldBlock(appUid: Int, trackerId: String?): Boolean { + return whitelistRepository.isBlockingEnabled && + !whitelistRepository.isWhiteListed(appUid, trackerId) + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt new file mode 100644 index 0000000..55efeb9 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.domain.usecases + +import foundation.e.advancedprivacy.data.repositories.AppListsRepository +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.trackers.data.StatsDatabase +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import java.time.temporal.TemporalUnit + +class StatisticsUseCase( + private val database: StatsDatabase, + private val appListsRepository: AppListsRepository +) { + private val _newDataAvailable = MutableSharedFlow() + val newDataAvailable: SharedFlow = _newDataAvailable + + suspend fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { + appListsRepository.getApp(appUid)?.let { app -> + database.logAccess(trackerId, app.apId, blocked) + _newDataAvailable.emit(Unit) + } + } + + fun getTrackersCallsOnPeriod( + periodsCount: Int, + periodUnit: TemporalUnit + ): List> { + return database.getTrackersCallsOnPeriod(periodsCount, periodUnit) + } + + fun getActiveTrackersByPeriod(periodsCount: Int, periodUnit: TemporalUnit): Int { + return database.getActiveTrackersByPeriod(periodsCount, periodUnit) + } + + fun getContactedTrackersCountByApp(): Map { + return database.getContactedTrackersCountByAppId().mapByAppIdToApp() + } + + fun getContactedTrackersCount(): Int { + return database.getContactedTrackersCount() + } + + fun getTrackers(apps: List?): List { + return database.getTrackers(apps?.map { it.apId }) + } + + fun getCallsByApps( + periodCount: Int, + periodUnit: TemporalUnit + ): Map> { + return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp() + } + + fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair { + return database.getCalls(app.apId, periodCount, periodUnit) + } + + fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): ApplicationDescription? { + return appListsRepository.getApp(database.getMostLeakedAppId(periodCount, periodUnit)) + } + + private fun Map.mapByAppIdToApp(): Map { + return entries.mapNotNull { (apId, value) -> + appListsRepository.getApp(apId)?.let { it to value } + }.toMap() + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt new file mode 100644 index 0000000..411b4ab --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.domain.usecases + +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.LinkedBlockingQueue + +class TrackersLogger( + private val statisticsUseCase: StatisticsUseCase, +) { + private val queue = LinkedBlockingQueue() + + fun logAccess(trackerId: String?, appUid: Int, wasBlocked: Boolean) { + queue.offer(DetectedTracker(trackerId, appUid, wasBlocked)) + } + + fun writeLogJob(scope: CoroutineScope): Job { + return scope.launch(Dispatchers.IO) { + while (isActive) { + runSuspendCatching { + logAccess(queue.take()) + }.onFailure { + Timber.e(it, "writeLogLoop detectedTrackersQueue.take() interrupted: ") + } + } + } + } + + private suspend fun logAccess(detectedTracker: DetectedTracker) { + statisticsUseCase.logAccess( + detectedTracker.trackerId, + detectedTracker.appUid, + detectedTracker.wasBlocked + ) + } + + inner class DetectedTracker(var trackerId: String?, var appUid: Int, var wasBlocked: Boolean) +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt new file mode 100644 index 0000000..3593dbb --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt @@ -0,0 +1,29 @@ +package foundation.e.advancedprivacy.trackers.domain.usecases + +import foundation.e.advancedprivacy.data.repositories.ETrackersApi +import foundation.e.advancedprivacy.data.repositories.RemoteTrackersListRepository +import foundation.e.advancedprivacy.trackers.data.TrackersRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +class UpdateTrackerListUseCase( + private val remoteTrackersListRepository: RemoteTrackersListRepository, + private val trackersRepository: TrackersRepository, + private val coroutineScope: CoroutineScope, + +) { + fun updateTrackers() = coroutineScope.launch { + update() + } + + suspend fun update() { + val api = ETrackersApi.build() + try { + remoteTrackersListRepository.saveData(trackersRepository.eTrackerFile, api.trackers()) + trackersRepository.initTrackersFile() + } catch (e: Exception) { + Timber.e("While updating trackers", e) + } + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt new file mode 100644 index 0000000..25539e1 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.services + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import foundation.e.advancedprivacy.trackers.domain.usecases.DNSBlocker +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import org.koin.java.KoinJavaComponent.get + +class DNSBlockerService : Service() { + companion object { + const val ACTION_START = "foundation.e.privacymodules.trackers.intent.action.START" + const val EXTRA_ENABLE_NOTIFICATION = + "foundation.e.privacymodules.trackers.intent.extra.ENABLED_NOTIFICATION" + } + + private var coroutineScope = CoroutineScope(Dispatchers.IO) + private var dnsBlocker: DNSBlocker? = null + + override fun onBind(intent: Intent): IBinder? { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (ACTION_START == intent?.action) { + if (intent.getBooleanExtra(EXTRA_ENABLE_NOTIFICATION, true)) { + ForegroundStarter.startForeground(this) + } + stop() + start() + } + return START_REDELIVER_INTENT + } + + private fun start() { + coroutineScope = CoroutineScope(Dispatchers.IO) + get(DNSBlocker::class.java).apply { + this@DNSBlockerService.dnsBlocker = this + trackersLogger.writeLogJob(coroutineScope) + listenJob(coroutineScope) + } + } + + private fun stop() { + kotlin.runCatching { coroutineScope.cancel() } + dnsBlocker = null + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/ForegroundStarter.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/ForegroundStarter.kt new file mode 100644 index 0000000..a0cea43 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/ForegroundStarter.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.services + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.os.Build + +object ForegroundStarter { + private const val NOTIFICATION_CHANNEL_ID = "blocker_service" + fun startForeground(service: Service) { + val mNotificationManager = + service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= 26) { + mNotificationManager.createNotificationChannel( + NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_LOW + ) + ) + val notification = Notification.Builder(service, NOTIFICATION_CHANNEL_ID) + .setContentTitle("Trackers filter").build() + service.startForeground(1337, notification) + } + } +} diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/UpdateTrackersWorker.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/UpdateTrackersWorker.kt new file mode 100644 index 0000000..50aa082 --- /dev/null +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/UpdateTrackersWorker.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.services + +import android.content.Context +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import foundation.e.advancedprivacy.trackers.domain.usecases.UpdateTrackerListUseCase +import org.koin.java.KoinJavaComponent.get +import java.util.concurrent.TimeUnit + +class UpdateTrackersWorker(appContext: Context, workerParams: WorkerParameters) : + CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + val updateTrackersUsecase: UpdateTrackerListUseCase = get(UpdateTrackerListUseCase::class.java) + + updateTrackersUsecase.updateTrackers() + return Result.success() + } + + companion object { + private val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + fun periodicUpdate(context: Context) { + val request = PeriodicWorkRequestBuilder( + 7, TimeUnit.DAYS + ) + .setConstraints(constraints).build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + UpdateTrackersWorker::class.qualifiedName ?: "", + ExistingPeriodicWorkPolicy.KEEP, + request + ) + } + } +} diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt deleted file mode 100644 index 44793a4..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers - -import android.content.Context -import android.content.pm.PackageManager -import android.net.LocalServerSocket -import android.system.ErrnoException -import android.system.Os -import android.system.OsConstants -import android.util.Log -import foundation.e.privacymodules.trackers.data.TrackersRepository -import foundation.e.privacymodules.trackers.data.WhitelistRepository -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.io.PrintWriter - -class DNSBlockerRunnable( - context: Context, - private val trackersLogger: TrackersLogger, - private val trackersRepository: TrackersRepository, - private val whitelistRepository: WhitelistRepository -) : Runnable { - var resolverReceiver: LocalServerSocket? = null - var stopped = false - private var eBrowserAppUid = -1 - - companion object { - private const val SOCKET_NAME = "foundation.e.advancedprivacy" - private const val E_BROWSER_DOT_SERVER = "chrome.cloudflare-dns.com" - private const val TAG = "DNSBlockerRunnable" - } - - init { - initEBrowserDoTFix(context) - } - - @Synchronized - fun stop() { - stopped = true - closeSocket() - } - - private fun closeSocket() { - // Known bug and workaround that LocalServerSocket::close is not working well - // https://issuetracker.google.com/issues/36945762 - if (resolverReceiver != null) { - try { - Os.shutdown(resolverReceiver!!.fileDescriptor, OsConstants.SHUT_RDWR) - resolverReceiver!!.close() - resolverReceiver = null - } catch (e: ErrnoException) { - if (e.errno != OsConstants.EBADF) { - Log.w(TAG, "Socket already closed") - } else { - Log.e(TAG, "Exception: cannot close DNS port on stop $SOCKET_NAME !", e) - } - } catch (e: Exception) { - Log.e(TAG, "Exception: cannot close DNS port on stop $SOCKET_NAME !", e) - } - } - } - - override fun run() { - val resolverReceiver = try { - LocalServerSocket(SOCKET_NAME) - } catch (eio: IOException) { - Log.e(TAG, "Exception:Cannot open DNS port $SOCKET_NAME !", eio) - return - } - - this.resolverReceiver = resolverReceiver - Log.d(TAG, "DNSFilterProxy running on port $SOCKET_NAME !") - - while (!stopped) { - try { - val socket = resolverReceiver.accept() - val reader = BufferedReader(InputStreamReader(socket.inputStream)) - val line = reader.readLine() - val params = line.split(",").toTypedArray() - val output = socket.outputStream - val writer = PrintWriter(output, true) - val domainName = params[0] - val appUid = params[1].toInt() - var isBlocked = false - if (isEBrowserDoTBlockFix(appUid, domainName)) { - isBlocked = true - } else if (trackersRepository.isTracker(domainName)) { - val trackerId = trackersRepository.getTrackerId(domainName) - if (shouldBlock(appUid, trackerId)) { - writer.println("block") - isBlocked = true - } - trackersLogger.logAccess(trackerId, appUid, isBlocked) - } - if (!isBlocked) { - writer.println("pass") - } - socket.close() - // Printing bufferedreader data - } catch (e: IOException) { - Log.w(TAG, "Exception while listening DNS resolver", e) - } - } - } - - private fun initEBrowserDoTFix(context: Context) { - try { - eBrowserAppUid = - context.packageManager.getApplicationInfo("foundation.e.browser", 0).uid - } catch (e: PackageManager.NameNotFoundException) { - Log.i(TAG, "no E Browser package found.") - } - } - - private fun isEBrowserDoTBlockFix(appUid: Int, hostname: String): Boolean { - return appUid == eBrowserAppUid && E_BROWSER_DOT_SERVER == hostname - } - - private fun shouldBlock(appUid: Int, trackerId: String?): Boolean { - return whitelistRepository.isBlockingEnabled && - !whitelistRepository.isWhiteListed(appUid, trackerId) - } -} diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerService.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerService.kt deleted file mode 100644 index c2ad16b..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerService.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import foundation.e.privacymodules.trackers.data.TrackersRepository -import foundation.e.privacymodules.trackers.data.WhitelistRepository - -class DNSBlockerService : Service() { - private var trackersLogger: TrackersLogger? = null - - companion object { - private const val TAG = "DNSBlockerService" - private var sDNSBlocker: DNSBlockerRunnable? = null - const val ACTION_START = "foundation.e.privacymodules.trackers.intent.action.START" - const val EXTRA_ENABLE_NOTIFICATION = - "foundation.e.privacymodules.trackers.intent.extra.ENABLED_NOTIFICATION" - } - - override fun onBind(intent: Intent): IBinder? { - // TODO: Return the communication channel to the service. - throw UnsupportedOperationException("Not yet implemented") - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (ACTION_START == intent?.action) { - if (intent.getBooleanExtra(EXTRA_ENABLE_NOTIFICATION, true)) { - ForegroundStarter.startForeground(this) - } - stop() - start() - } - return START_REDELIVER_INTENT - } - - private fun start() { - try { - val trackersLogger = TrackersLogger(this) - this.trackersLogger = trackersLogger - - sDNSBlocker = DNSBlockerRunnable( - this, - trackersLogger, - TrackersRepository.getInstance(), - WhitelistRepository.getInstance(this) - ) - Thread(sDNSBlocker).start() - } catch (e: Exception) { - Log.e(TAG, "Error while starting DNSBlocker service", e) - stop() - } - } - - private fun stop() { - sDNSBlocker?.stop() - sDNSBlocker = null - - trackersLogger?.stop() - trackersLogger = null - } -} diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/ForegroundStarter.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/ForegroundStarter.kt deleted file mode 100644 index 69b4f28..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/ForegroundStarter.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.os.Build - -object ForegroundStarter { - private const val NOTIFICATION_CHANNEL_ID = "blocker_service" - fun startForeground(service: Service) { - val mNotificationManager = - service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= 26) { - mNotificationManager.createNotificationChannel( - NotificationChannel( - NOTIFICATION_CHANNEL_ID, - NOTIFICATION_CHANNEL_ID, - NotificationManager.IMPORTANCE_LOW - ) - ) - val notification = Notification.Builder(service, NOTIFICATION_CHANNEL_ID) - .setContentTitle("Trackers filter").build() - service.startForeground(1337, notification) - } - } -} diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt deleted file mode 100644 index f3c4745..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers - -import android.content.Context -import android.util.Log -import foundation.e.privacymodules.trackers.data.StatsRepository -import java.util.concurrent.LinkedBlockingQueue - -class TrackersLogger(context: Context) { - private val statsRepository = StatsRepository.getInstance(context) - private val queue = LinkedBlockingQueue() - private var stopped = false - - companion object { - private const val TAG = "TrackerModule" - } - - init { - startWriteLogLoop() - } - - fun stop() { - stopped = true - } - - fun logAccess(trackerId: String?, appUid: Int, wasBlocked: Boolean) { - queue.offer(DetectedTracker(trackerId, appUid, wasBlocked)) - } - - private fun startWriteLogLoop() { - val writeLogRunner = Runnable { - while (!stopped) { - try { - logAccess(queue.take()) - } catch (e: InterruptedException) { - Log.e(TAG, "writeLogLoop detectedTrackersQueue.take() interrupted: ", e) - } - } - } - Thread(writeLogRunner).start() - } - - fun logAccess(detectedTracker: DetectedTracker) { - statsRepository.logAccess( - detectedTracker.trackerId, - detectedTracker.appUid, - detectedTracker.wasBlocked - ) - } - - inner class DetectedTracker(var trackerId: String?, var appUid: Int, var wasBlocked: Boolean) -} 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 deleted file mode 100644 index 7463b22..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 - -class BlockTrackersPrivacyModule(context: Context) : IBlockTrackersPrivacyModule { - private val mListeners = mutableListOf() - private val trackersRepository = TrackersRepository.getInstance() - private val whitelistRepository = WhitelistRepository.getInstance(context) - - companion object { - private var instance: BlockTrackersPrivacyModule? = null - - fun getInstance(context: Context): BlockTrackersPrivacyModule { - return instance ?: BlockTrackersPrivacyModule(context).apply { instance = this } - } - } - - override fun addListener(listener: IBlockTrackersPrivacyModule.Listener) { - mListeners.add(listener) - } - - override fun clearListeners() { - mListeners.clear() - } - - override fun disableBlocking() { - whitelistRepository.isBlockingEnabled = false - mListeners.forEach { listener -> listener.onBlockingToggle(false) } - } - - override fun enableBlocking() { - whitelistRepository.isBlockingEnabled = true - mListeners.forEach { listener -> listener.onBlockingToggle(true) } - } - - override fun getWhiteList(app: ApplicationDescription): List { - return whitelistRepository.getWhiteListForApp(app).mapNotNull { - trackersRepository.getTracker(it) - } - } - - override fun getWhiteListedApp(): List { - return whitelistRepository.getWhiteListedApp() - } - - override fun isBlockingEnabled(): Boolean { - return whitelistRepository.isBlockingEnabled - } - - override fun isWhiteListEmpty(): Boolean { - return whitelistRepository.areWhiteListEmpty() - } - - override fun isWhitelisted(app: ApplicationDescription): Boolean { - return whitelistRepository.isAppWhiteListed(app) - } - - override fun removeListener(listener: IBlockTrackersPrivacyModule.Listener) { - mListeners.remove(listener) - } - - override fun setWhiteListed( - tracker: Tracker, - app: ApplicationDescription, - isWhiteListed: Boolean - ) { - whitelistRepository.setWhiteListed(tracker, app.apId, 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 deleted file mode 100644 index 3547b8e..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers.api - -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -/** - * Manage trackers blocking and whitelisting. - */ -interface IBlockTrackersPrivacyModule { - - /** - * Get the state of the blockin module - * @return true when blocking is enabled, false otherwise. - */ - fun isBlockingEnabled(): Boolean - - /** - * Enable blocking, using the previously configured whitelists - */ - fun enableBlocking() - - /** - * Disable blocking - */ - fun disableBlocking() - - /** - * Set or unset in whitelist the App with the specified uid. - * @param app the ApplicationDescription of the app - * @param isWhiteListed true, the app will appears in whitelist, false, it won't - */ - 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 app the ApplicationDescription of the app - * @param isWhiteListed true, the app will appears in whitelist, false, it won't - */ - fun setWhiteListed(tracker: Tracker, app: ApplicationDescription, isWhiteListed: Boolean) - - /** - * Return true if nothing has been added to the whitelist : everything is blocked. - */ - fun isWhiteListEmpty(): Boolean - - /** - * Return the white listed App, by their UID - */ - fun getWhiteListedApp(): List - - /** - * Return true if the App is whitelisted for trackers blocking. - */ - fun isWhitelisted(app: ApplicationDescription): Boolean - - /** - * List the white listed trackers for an App specified by it uid - */ - fun getWhiteList(app: ApplicationDescription): List - - fun clearWhiteList(app: ApplicationDescription) - - /** - * Callback interface to get updates about the state of the Block trackers module. - */ - interface Listener { - - /** - * Called when the trackers blocking is activated or deactivated. - * @param isBlocking true when activated, false otherwise. - */ - fun onBlockingToggle(isBlocking: Boolean) - } - - fun addListener(listener: Listener) - - fun removeListener(listener: Listener) - - fun clearListeners() -} 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 deleted file mode 100644 index 8aaed4a..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers.api - -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -/** - * Get reporting about trackers calls. - */ -interface ITrackTrackersPrivacyModule { - - fun start( - trackers: List, - getAppByUid: (Int) -> ApplicationDescription?, - getAppByAPId: (String) -> ApplicationDescription?, - enableNotification: Boolean = true - ) - - /** - * List all the trackers encountered for a specific app. - */ - fun getTrackersForApp(app: ApplicationDescription): List - - /** - * List all the trackers encountere trackers since "ever", for the given [appUids], - * or all apps if [appUids] is null - */ - fun getTrackers(apps: List? = null): List - - /** - * Return the number of encountered trackers since "ever", for the given [appUids], - * or all apps if [appUids] is null - */ - fun getTrackersCount(): Int - - /** - * Return the number of encountere trackers since "ever", for each app uid. - */ - fun getTrackersCountByApp(): Map - - /** - * Return the number of encountered trackers for the last 24 hours - */ - fun getPastDayTrackersCount(): Int - - /** - * Return the number of encountered trackers for the last month - */ - fun getPastMonthTrackersCount(): Int - - /** - * Return the number of encountered trackers for the last year - */ - fun getPastYearTrackersCount(): Int - - /** - * Return number of trackers calls by hours, for the last 24hours. - * @return list of 24 numbers of trackers calls by hours - */ - fun getPastDayTrackersCalls(): List> - - /** - * Return number of trackers calls by day, for the last 30 days. - * @return list of 30 numbers of trackers calls by day - */ - fun getPastMonthTrackersCalls(): List> - - /** - * Return number of trackers calls by month, for the last 12 month. - * @return list of 12 numbers of trackers calls by month - */ - fun getPastYearTrackersCalls(): List> - - fun getPastDayTrackersCallsByApps(): Map> - - fun getPastDayTrackersCallsForApp(app: ApplicationDescription): Pair - - fun getPastDayMostLeakedApp(): ApplicationDescription? - - interface Listener { - - /** - * Called when a new tracker attempt is logged. Consumer may choose to call other methods - * to refresh the data. - */ - fun onNewData() - } - - fun addListener(listener: Listener) - - fun removeListener(listener: Listener) - - fun clearListeners() -} 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 deleted file mode 100644 index 5fc5b6b..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 { - private val statsRepository = StatsRepository.getInstance(context) - private val listeners: MutableList = mutableListOf() - - companion object { - private var instance: TrackTrackersPrivacyModule? = null - - fun getInstance(context: Context): TrackTrackersPrivacyModule { - return instance ?: TrackTrackersPrivacyModule(context).apply { instance = this } - } - } - - init { - statsRepository.setNewDataCallback { - listeners.forEach { listener -> listener.onNewData() } - } - } - - override fun start( - trackers: List, - 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) - context.startService(intent) - } - - override fun getPastDayTrackersCalls(): List> { - return statsRepository.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS) - } - - override fun getPastMonthTrackersCalls(): List> { - return statsRepository.getTrackersCallsOnPeriod(30, ChronoUnit.DAYS) - } - - override fun getPastYearTrackersCalls(): List> { - return statsRepository.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS) - } - - override fun getTrackersCount(): Int { - return statsRepository.getContactedTrackersCount() - } - - override fun getTrackersCountByApp(): Map { - return statsRepository.getContactedTrackersCountByApp() - } - - override fun getTrackersForApp(app: ApplicationDescription): List { - return statsRepository.getTrackers(listOf(app)) - } - - override fun getTrackers(apps: List?): List { - return statsRepository.getTrackers(apps) - } - - override fun getPastDayTrackersCount(): Int { - return statsRepository.getActiveTrackersByPeriod(24, ChronoUnit.HOURS) - } - - override fun getPastMonthTrackersCount(): Int { - return statsRepository.getActiveTrackersByPeriod(30, ChronoUnit.DAYS) - } - - override fun getPastYearTrackersCount(): Int { - return statsRepository.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS) - } - - override fun getPastDayMostLeakedApp(): ApplicationDescription? { - return statsRepository.getMostLeakedApp(24, ChronoUnit.HOURS) - } - - override fun getPastDayTrackersCallsByApps(): Map> { - return statsRepository.getCallsByApps(24, ChronoUnit.HOURS) - } - - override fun getPastDayTrackersCallsForApp(app: ApplicationDescription): Pair { - return statsRepository.getCalls(app, 24, ChronoUnit.HOURS) - } - - override fun addListener(listener: ITrackTrackersPrivacyModule.Listener) { - listeners.add(listener) - } - - override fun removeListener(listener: ITrackTrackersPrivacyModule.Listener) { - listeners.remove(listener) - } - - override fun clearListeners() { - listeners.clear() - } -} diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/Tracker.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/Tracker.kt deleted file mode 100644 index 2da5b16..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/Tracker.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers.api - -/** - * Describe a tracker. - */ -data class Tracker( - val id: String, - val hostnames: Set, - val label: String, - val exodusId: String? -) 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 deleted file mode 100644 index 4d287d4..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers.data - -import android.content.ContentValues -import android.content.Context -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_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 -import java.time.temporal.TemporalUnit -import java.util.concurrent.TimeUnit - -class StatsDatabase(context: Context) : - SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - - companion object { - 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_TRACKER TEXT," + - "$COLUMN_NAME_NUMBER_CONTACTED 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_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_TRACKER, - COLUMN_NAME_NUMBER_CONTACTED, - COLUMN_NAME_NUMBER_BLOCKED, - COLUMN_NAME_APPID - ) - - private val lock = Any() - private val trackersRepository = TrackersRepository.getInstance() - - override fun onCreate(db: SQLiteDatabase) { - db.execSQL(SQL_CREATE_TABLE) - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion == 1 && newVersion == 2) { - MIGRATE_1_2.forEach(db::execSQL) - } else { - Timber.e( - "Unexpected database versions: oldVersion: $oldVersion ; newVersion: $newVersion" - ) - } - } - - private fun getCallsByPeriod( - periodsCount: Int, - periodUnit: TemporalUnit, - sqlitePeriodFormat: String - ): Map> { - synchronized(lock) { - val minTimestamp = getPeriodStartTs(periodsCount, periodUnit) - val db = readableDatabase - val selection = "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + minTimestamp) - - val projection = ( - "$COLUMN_NAME_TIMESTAMP, " + - "STRFTIME('$sqlitePeriodFormat', DATETIME($COLUMN_NAME_TIMESTAMP, 'unixepoch', 'localtime')) $PROJECTION_NAME_PERIOD," + - "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 $PROJECTION_NAME_PERIOD" + - " ORDER BY $COLUMN_NAME_TIMESTAMP DESC LIMIT $periodsCount", - selectionArg - ) - val callsByPeriod = HashMap>() - while (cursor.moveToNext()) { - val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) - val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) - callsByPeriod[cursor.getString(PROJECTION_NAME_PERIOD)] = blocked to contacted - blocked - } - cursor.close() - db.close() - return callsByPeriod - } - } - - private fun callsByPeriodToPeriodsList( - callsByPeriod: Map>, - periodsCount: Int, - periodUnit: TemporalUnit, - javaPeriodFormat: String - ): List> { - var currentDate = ZonedDateTime.now().minus(periodsCount.toLong(), periodUnit) - val formatter = DateTimeFormatter.ofPattern(javaPeriodFormat) - val calls = mutableListOf>() - for (i in 0 until periodsCount) { - currentDate = currentDate.plus(1, periodUnit) - val currentPeriod = formatter.format(currentDate) - calls.add(callsByPeriod.getOrDefault(currentPeriod, 0 to 0)) - } - return calls - } - - fun getTrackersCallsOnPeriod( - periodsCount: Int, - periodUnit: TemporalUnit - ): List> { - var sqlitePeriodFormat = "%Y%m" - var javaPeriodFormat = "yyyyMM" - if (periodUnit === ChronoUnit.MONTHS) { - sqlitePeriodFormat = "%Y%m" - javaPeriodFormat = "yyyyMM" - } else if (periodUnit === ChronoUnit.DAYS) { - sqlitePeriodFormat = "%Y%m%d" - javaPeriodFormat = "yyyyMMdd" - } else if (periodUnit === ChronoUnit.HOURS) { - sqlitePeriodFormat = "%Y%m%d%H" - javaPeriodFormat = "yyyyMMddHH" - } - val callsByPeriod = getCallsByPeriod(periodsCount, periodUnit, sqlitePeriodFormat) - return callsByPeriodToPeriodsList(callsByPeriod, periodsCount, periodUnit, javaPeriodFormat) - } - - fun getActiveTrackersByPeriod(periodsCount: Int, periodUnit: TemporalUnit): Int { - synchronized(lock) { - val minTimestamp = getPeriodStartTs(periodsCount, periodUnit) - val db = writableDatabase - val selection = "$COLUMN_NAME_TIMESTAMP >= ? AND " + - "$COLUMN_NAME_NUMBER_CONTACTED > $COLUMN_NAME_NUMBER_BLOCKED" - val selectionArg = arrayOf("" + minTimestamp) - val projection = - "COUNT(DISTINCT $COLUMN_NAME_TRACKER) $PROJECTION_NAME_TRACKERS_COUNT" - - val cursor = db.rawQuery( - "SELECT $projection FROM $TABLE_NAME WHERE $selection", - selectionArg - ) - var count = 0 - if (cursor.moveToNext()) { - count = cursor.getInt(0) - } - cursor.close() - db.close() - return count - } - } - - fun getContactedTrackersCount(): Int { - synchronized(lock) { - val db = readableDatabase - var query = "SELECT DISTINCT $COLUMN_NAME_TRACKER FROM $TABLE_NAME" - - val cursor = db.rawQuery(query, arrayOf()) - var count = 0 - while (cursor.moveToNext()) { - trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { - count++ - } - } - cursor.close() - db.close() - return count - } - } - - fun getContactedTrackersCountByAppId(): Map { - synchronized(lock) { - val db = readableDatabase - val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER" - val cursor = db.rawQuery( - "SELECT DISTINCT $projection FROM $TABLE_NAME", // + - arrayOf() - ) - val countByApp = mutableMapOf() - while (cursor.moveToNext()) { - trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { - val appId = cursor.getString(COLUMN_NAME_APPID) - countByApp[appId] = countByApp.getOrDefault(appId, 0) + 1 - } - } - cursor.close() - db.close() - return countByApp - } - } - - fun getCallsByAppIds(periodCount: Int, periodUnit: TemporalUnit): Map> { - synchronized(lock) { - val minTimestamp = getPeriodStartTs(periodCount, periodUnit) - val db = readableDatabase - val selection = "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + minTimestamp) - 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_APPID", - selectionArg - ) - val callsByApp = HashMap>() - while (cursor.moveToNext()) { - val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) - val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) - callsByApp[cursor.getString(COLUMN_NAME_APPID)] = blocked to contacted - blocked - } - cursor.close() - db.close() - return callsByApp - } - } - - fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair { - synchronized(lock) { - val minTimestamp = getPeriodStartTs(periodCount, periodUnit) - val db = readableDatabase - val selection = "$COLUMN_NAME_APPID = ? AND " + - "$COLUMN_NAME_TIMESTAMP >= ?" - 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" - val cursor = db.rawQuery( - "SELECT $projection FROM $TABLE_NAME WHERE $selection", - selectionArg - ) - var calls: Pair = 0 to 0 - if (cursor.moveToNext()) { - val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) - val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) - calls = blocked to contacted - blocked - } - cursor.close() - db.close() - return calls - } - } - - 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_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_APPID" + - " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1", - selectionArg - ) - var appId = "" - if (cursor.moveToNext()) { - appId = cursor.getString(COLUMN_NAME_APPID) - } - cursor.close() - db.close() - return appId - } - } - - fun logAccess(trackerId: String?, appId: String, blocked: Boolean) { - synchronized(lock) { - val currentHour = getCurrentHourTs() - val db = writableDatabase - val values = ContentValues() - values.put(COLUMN_NAME_APPID, appId) - values.put(COLUMN_NAME_TRACKER, trackerId) - values.put(COLUMN_NAME_TIMESTAMP, currentHour) - - /*String query = "UPDATE product SET "+COLUMN_NAME_NUMBER_CONTACTED+" = "+COLUMN_NAME_NUMBER_CONTACTED+" + 1 "; - if(blocked) - query+=COLUMN_NAME_NUMBER_BLOCKED+" = "+COLUMN_NAME_NUMBER_BLOCKED+" + 1 "; -*/ - val selection = "$COLUMN_NAME_TIMESTAMP = ? AND " + - "$COLUMN_NAME_APPID = ? AND " + - "$COLUMN_NAME_TRACKER = ? " - val selectionArg = arrayOf("" + currentHour, "" + appId, trackerId) - val cursor = db.query( - TABLE_NAME, - projection, - selection, - selectionArg, - null, - null, - null - ) - if (cursor.count > 0) { - cursor.moveToFirst() - val entry = cursorToEntry(cursor) - if (blocked) values.put( - COLUMN_NAME_NUMBER_BLOCKED, - entry.sum_blocked + 1 - ) else values.put(COLUMN_NAME_NUMBER_BLOCKED, entry.sum_blocked) - values.put(COLUMN_NAME_NUMBER_CONTACTED, entry.sum_contacted + 1) - db.update(TABLE_NAME, values, selection, selectionArg) - - // db.execSQL(query, new String[]{""+hour, ""+day, ""+month, ""+year, ""+appUid, ""+trackerId}); - } else { - if (blocked) values.put( - COLUMN_NAME_NUMBER_BLOCKED, - 1 - ) else values.put(COLUMN_NAME_NUMBER_BLOCKED, 0) - values.put(COLUMN_NAME_NUMBER_CONTACTED, 1) - db.insert(TABLE_NAME, null, values) - } - cursor.close() - db.close() - } - } - - private fun cursorToEntry(cursor: Cursor): StatEntry { - val entry = StatEntry() - 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(appIds: List?): List { - synchronized(lock) { - val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID) - var selection: String? = null - - var selectionArg: Array? = null - appIds?.let { appIds -> - selection = "$COLUMN_NAME_APPID IN (${appIds.joinToString(", ") { "'$it'" }})" - selectionArg = arrayOf() - } - - val db = readableDatabase - val cursor = db.query( - true, - TABLE_NAME, - columns, - selection, - selectionArg, - null, - null, - null, - null - ) - val trackers: MutableList = ArrayList() - while (cursor.moveToNext()) { - val trackerId = cursor.getString(COLUMN_NAME_TRACKER) - val tracker = trackersRepository.getTracker(trackerId) - if (tracker != null) { - trackers.add(tracker) - } - } - cursor.close() - db.close() - return trackers - } - } - - class StatEntry { - var appId = "" - var sum_contacted = 0 - var sum_blocked = 0 - var timestamp: Long = 0 - var tracker = 0 - } - - private fun getCurrentHourTs(): Long { - val hourInMs = TimeUnit.HOURS.toMillis(1L) - val hourInS = TimeUnit.HOURS.toSeconds(1L) - return System.currentTimeMillis() / hourInMs * hourInS - } - - private fun getPeriodStartTs( - periodsCount: Int, - periodUnit: TemporalUnit - ): Long { - var start = ZonedDateTime.now() - .minus(periodsCount.toLong(), periodUnit) - .plus(1, periodUnit) - var truncatePeriodUnit = periodUnit - if (periodUnit === ChronoUnit.MONTHS) { - start = start.withDayOfMonth(1) - truncatePeriodUnit = ChronoUnit.DAYS - } - return start.truncatedTo(truncatePeriodUnit).toEpochSecond() - } - - private fun Cursor.getInt(columnName: String): Int { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex >= 0) getInt(columnIndex) else 0 - } - - private fun Cursor.getLong(columnName: String): Long { - val columnIndex = getColumnIndex(columnName) - return if (columnIndex >= 0) getLong(columnIndex) else 0 - } - - private fun Cursor.getString(columnName: String): String { - val columnIndex = getColumnIndex(columnName) - 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 deleted file mode 100644 index 8f02adb..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 - fun getInstance(context: Context): StatsRepository { - return instance ?: StatsRepository(context).apply { instance = this } - } - } - - fun setAppGetters( - getAppByUid: (Int) -> ApplicationDescription?, - getAppByAPId: (String) -> ApplicationDescription? - ) { - this.getAppByUid = getAppByUid - this.getAppByAPId = getAppByAPId - } - - init { - database = StatsDatabase(context) - } - - fun setNewDataCallback(callback: () -> Unit) { - newDataCallback = callback - } - - fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { - getAppByUid?.invoke(appUid)?.let { app -> - database.logAccess(trackerId, app.apId, blocked) - newDataCallback?.invoke() - } - } - - fun getTrackersCallsOnPeriod( - periodsCount: Int, - periodUnit: TemporalUnit - ): List> { - return database.getTrackersCallsOnPeriod(periodsCount, periodUnit) - } - - fun getActiveTrackersByPeriod(periodsCount: Int, periodUnit: TemporalUnit): Int { - return database.getActiveTrackersByPeriod(periodsCount, periodUnit) - } - - fun getContactedTrackersCountByApp(): Map { - return database.getContactedTrackersCountByAppId().mapByAppIdToApp() - } - - fun getContactedTrackersCount(): Int { - return database.getContactedTrackersCount() - } - - fun getTrackers(apps: List?): List { - return database.getTrackers(apps?.map { it.apId }) - } - - fun getCallsByApps( - periodCount: Int, - periodUnit: TemporalUnit - ): Map> { - return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp() - } - - fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair { - return database.getCalls(app.apId, periodCount, periodUnit) - } - - fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): ApplicationDescription? { - return getAppByAPId?.invoke(database.getMostLeakedAppId(periodCount, periodUnit)) - } - - private fun Map.mapByAppIdToApp(): Map { - 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/TrackersRepository.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/TrackersRepository.kt deleted file mode 100644 index 994bccf..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/TrackersRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.privacymodules.trackers.data - -import foundation.e.privacymodules.trackers.api.Tracker - -class TrackersRepository private constructor() { - private var trackersById: Map = HashMap() - private var hostnameToId: Map = HashMap() - - companion object { - private var instance: TrackersRepository? = null - fun getInstance(): TrackersRepository { - return instance ?: TrackersRepository().apply { instance = this } - } - } - - fun setTrackersList(list: List) { - val trackersById: MutableMap = HashMap() - val hostnameToId: MutableMap = HashMap() - list.forEach { tracker -> - trackersById[tracker.id] = tracker - for (hostname in tracker.hostnames) { - hostnameToId[hostname] = tracker.id - } - } - this.trackersById = trackersById - this.hostnameToId = hostnameToId - } - - fun isTracker(hostname: String?): Boolean { - return hostnameToId.containsKey(hostname) - } - - fun getTrackerId(hostname: String?): String? { - return hostnameToId[hostname] - } - - fun getTracker(id: String?): Tracker? { - return trackersById[id] - } -} 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 deleted file mode 100644 index 2763d06..0000000 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -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 var appsWhitelist: Set = HashSet() - private var appUidsWhitelist: Set = HashSet() - - private var trackersWhitelistByApp: MutableMap> = HashMap() - private var trackersWhitelistByUid: Map> = HashMap() - - private val prefs: SharedPreferences - private var getAppByAPId: ((String) -> ApplicationDescription?)? = null - - companion object { - 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 } - } - } - - init { - prefs = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) - 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_BLOCKING_ENABLED, false) - reloadAppsWhiteList() - reloadAllAppTrackersWhiteList() - } - - private fun reloadAppsWhiteList() { - appsWhitelist = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet()) ?: HashSet() - appUidsWhitelist = appsWhitelist - .mapNotNull { apId -> getAppByAPId?.invoke(apId)?.uid } - .toSet() - } - - private fun refreshAppUidTrackersWhiteList() { - trackersWhitelistByUid = trackersWhitelistByApp.mapNotNull { (apId, value) -> - getAppByAPId?.invoke(apId)?.uid?.let { uid -> - uid to value - } - }.toMap() - } - private fun reloadAllAppTrackersWhiteList() { - val map: MutableMap> = HashMap() - prefs.all.keys.forEach { key -> - if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) { - 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_BLOCKING_ENABLED, enabled).apply() - field = enabled - } - - fun setWhiteListed(apId: String, isWhiteListed: Boolean) { - val current = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet())?.toHashSet() ?: HashSet() - - if (isWhiteListed) { - current.add(apId) - } else { - current.remove(apId) - } - prefs.edit().putStringSet(KEY_APPS_WHITELIST, current).commit() - reloadAppsWhiteList() - } - - private fun buildAppTrackersKey(apId: String): String { - return KEY_APP_TRACKERS_WHITELIST_PREFIX + apId - } - - 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) - } - refreshAppUidTrackersWhiteList() - prefs.edit().putStringSet(buildAppTrackersKey(apId), trackers).commit() - } - - fun isAppWhiteListed(app: ApplicationDescription): Boolean { - return appsWhitelist.contains(app.apId) - } - - 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() } - } - - fun getWhiteListedApp(): List { - return getAppByAPId?.let { - appsWhitelist.mapNotNull(it) - } ?: emptyList() - } - - fun getWhiteListForApp(app: ApplicationDescription): List { - return trackersWhitelistByApp[app.apId]?.toList() ?: emptyList() - } - - fun clearWhiteList(apId: String) { - trackersWhitelistByApp.remove(apId) - refreshAppUidTrackersWhiteList() - prefs.edit().remove(buildAppTrackersKey(apId)).commit() - } -} -- cgit v1.2.1