summaryrefslogtreecommitdiff
path: root/trackers/src/main/java/foundation/e/advancedprivacy
diff options
context:
space:
mode:
Diffstat (limited to 'trackers/src/main/java/foundation/e/advancedprivacy')
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt72
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/ETrackersResponse.kt10
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/RemoteTrackersListRepository.kt61
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt461
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt109
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt195
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/entities/Tracker.kt28
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/DNSBlocker.kt143
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt86
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/TrackersLogger.kt60
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/UpdateTrackerListUseCase.kt29
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/DNSBlockerService.kt68
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/ForegroundStarter.kt45
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/services/UpdateTrackersWorker.kt60
14 files changed, 1427 insertions, 0 deletions
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 <https://www.gnu.org/licenses/>.
+ */
+
+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<ETracker>) {
+ data class ETracker(
+ val id: String?,
+ val hostnames: List<String>?,
+ 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 <https://www.gnu.org/licenses/>.
+ */
+
+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 <https://www.gnu.org/licenses/>.
+ */
+
+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<String, Pair<Int, Int>> {
+ 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<String, Pair<Int, Int>>()
+ 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<String, Pair<Int, Int>>,
+ periodsCount: Int,
+ periodUnit: TemporalUnit,
+ javaPeriodFormat: String
+ ): List<Pair<Int, Int>> {
+ var currentDate = ZonedDateTime.now().minus(periodsCount.toLong(), periodUnit)
+ val formatter = DateTimeFormatter.ofPattern(javaPeriodFormat)
+ val calls = mutableListOf<Pair<Int, Int>>()
+ 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<Pair<Int, Int>> {
+ 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<String, Int> {
+ 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<String, Int>()
+ 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<String, Pair<Int, Int>> {
+ synchronized(lock) {
+ val minTimestamp = getPeriodStartTs(periodCount, periodUnit)
+ val db = readableDatabase
+ val selection = "$COLUMN_NAME_TIMESTAMP >= ?"
+ val selectionArg = arrayOf("" + minTimestamp)
+ val projection = "$COLUMN_NAME_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<String, Pair<Int, Int>>()
+ while (cursor.moveToNext()) {
+ val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM)
+ val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM)
+ callsByApp[cursor.getString(COLUMN_NAME_APPID)] = blocked to contacted - blocked
+ }
+ cursor.close()
+ db.close()
+ return callsByApp
+ }
+ }
+
+ fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> {
+ synchronized(lock) {
+ val minTimestamp = getPeriodStartTs(periodCount, periodUnit)
+ val db = readableDatabase
+ val selection = "$COLUMN_NAME_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<Int, Int> = 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<String>?): List<Tracker> {
+ synchronized(lock) {
+ val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID)
+ var selection: String? = null
+
+ var selectionArg: Array<String>? = 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<Tracker> = 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 <https://www.gnu.org/licenses/>.
+ */
+
+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<String, Tracker> = HashMap()
+ private var hostnameToId: Map<String, String> = 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<Tracker> {
+ 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<Tracker>) {
+ val trackersById: MutableMap<String, Tracker> = HashMap()
+ val hostnameToId: MutableMap<String, String> = 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 <https://www.gnu.org/licenses/>.
+ */
+
+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<String> = HashSet()
+ private var appUidsWhitelist: Set<Int> = HashSet()
+
+ private var trackersWhitelistByApp: MutableMap<String, MutableSet<String>> = HashMap()
+ private var trackersWhitelistByUid: Map<Int, MutableSet<String>> = HashMap()
+
+ private val prefs: SharedPreferences
+
+ 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<String, MutableSet<String>> = 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<ApplicationDescription> {
+ return appsWhitelist.mapNotNull(appListsRepository::getApp)
+ }
+
+ fun getWhiteListForApp(app: ApplicationDescription): List<String> {
+ 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.trackers.domain.entities
+
+/**
+ * Describe a tracker.
+ */
+data class Tracker(
+ val id: String,
+ val hostnames: Set<String>,
+ 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 <https://www.gnu.org/licenses/>.
+ */
+
+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 <https://www.gnu.org/licenses/>.
+ */
+
+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<Unit>()
+ val newDataAvailable: SharedFlow<Unit> = _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<Pair<Int, Int>> {
+ return database.getTrackersCallsOnPeriod(periodsCount, periodUnit)
+ }
+
+ fun getActiveTrackersByPeriod(periodsCount: Int, periodUnit: TemporalUnit): Int {
+ return database.getActiveTrackersByPeriod(periodsCount, periodUnit)
+ }
+
+ fun getContactedTrackersCountByApp(): Map<ApplicationDescription, Int> {
+ return database.getContactedTrackersCountByAppId().mapByAppIdToApp()
+ }
+
+ fun getContactedTrackersCount(): Int {
+ return database.getContactedTrackersCount()
+ }
+
+ fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> {
+ return database.getTrackers(apps?.map { it.apId })
+ }
+
+ fun getCallsByApps(
+ periodCount: Int,
+ periodUnit: TemporalUnit
+ ): Map<ApplicationDescription, Pair<Int, Int>> {
+ return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp()
+ }
+
+ fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> {
+ return database.getCalls(app.apId, periodCount, periodUnit)
+ }
+
+ fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): ApplicationDescription? {
+ return appListsRepository.getApp(database.getMostLeakedAppId(periodCount, periodUnit))
+ }
+
+ private fun <K> Map<String, K>.mapByAppIdToApp(): Map<ApplicationDescription, K> {
+ 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 <https://www.gnu.org/licenses/>.
+ */
+
+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<DetectedTracker>()
+
+ 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 <https://www.gnu.org/licenses/>.
+ */
+
+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>(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 <https://www.gnu.org/licenses/>.
+ */
+
+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 <https://www.gnu.org/licenses/>.
+ */
+
+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<UpdateTrackersWorker>(
+ 7, TimeUnit.DAYS
+ )
+ .setConstraints(constraints).build()
+
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(
+ UpdateTrackersWorker::class.qualifiedName ?: "",
+ ExistingPeriodicWorkPolicy.KEEP,
+ request
+ )
+ }
+ }
+}