diff options
Diffstat (limited to 'trackers/src/main/java/foundation/e/advancedprivacy')
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 + ) + } + } +} |