diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-04-21 06:25:54 +0000 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-04-21 06:25:54 +0000 |
commit | 6068cebe972e000872e4780dd9f75680a3abf073 (patch) | |
tree | 4785f6b44d121f95c840020441687bce5777a44f | |
parent | 2df577ca97a674a4bd3875dc5137bb44df2c03ef (diff) |
6556: add AdvancedPrivacy App Id in trackers stats to avoid appUid aliasing
31 files changed, 764 insertions, 461 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt index 6ad84a7..aab81d5 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -47,6 +48,7 @@ import foundation.e.privacymodules.ipscrambler.IpScramblerModule import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule import foundation.e.privacymodules.permissions.PermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.permissions.data.ProfileType import foundation.e.privacymodules.trackers.api.BlockTrackersPrivacyModule import foundation.e.privacymodules.trackers.api.TrackTrackersPrivacyModule import kotlinx.coroutines.DelicateCoroutinesApi @@ -70,7 +72,9 @@ class DependencyContainer(val app: Application) { packageName = context.packageName, uid = Process.myUid(), label = context.resources.getString(R.string.app_name), - icon = null + icon = null, + profileId = -1, + profileType = ProfileType.MAIN ) } @@ -168,12 +172,12 @@ class ViewModelsFactory( override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { return when (modelClass) { AppTrackersViewModel::class.java -> { - val fallbackUid = android.os.Process.myPid() - val appUid = extras[DEFAULT_ARGS_KEY] - ?.getInt(AppTrackersFragment.PARAM_APP_UID, fallbackUid) ?: fallbackUid + val app = extras[DEFAULT_ARGS_KEY]?.getInt(AppTrackersFragment.PARAM_APP_UID)?.let { + appListUseCase.getApp(it) + } ?: appListUseCase.dummySystemApp AppTrackersViewModel( - appUid = appUid, + app = app, trackersStateUseCase = trackersStateUseCase, trackersStatisticsUseCase = trackersStatisticsUseCase, getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt index 7b09c51..2fbbc34 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS + * Copyright (C) 2021 E FOUNDATION, 2022 - 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 @@ -38,12 +38,12 @@ class AppsAdapter( val icon: ImageView = view.findViewById(R.id.icon) fun bind(item: AppWithCounts) { appName.text = item.label - counts.text = itemView.context.getString( + counts.text = if (item.trackersCount > 0) itemView.context.getString( R.string.trackers_app_trackers_counts, item.blockedTrackersCount, item.trackersCount, item.leaks - ) + ) else "" icon.setImageDrawable(item.icon) itemView.setOnClickListener { listener(item.uid) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt index a97888f..a4f7487 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 E FOUNDATION, 2022 MURENA SAS + * Copyright (C) 2022 E FOUNDATION, 2022 - 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 @@ -25,6 +25,7 @@ import android.content.pm.PackageInfo import foundation.e.privacycentralapp.R import foundation.e.privacymodules.permissions.PermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.permissions.data.ProfileType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class AppListsRepository( private val permissionsModule: PermissionsPrivacyModule, @@ -44,7 +46,7 @@ class AppListsRepository( private const val PNAME_INTENT_VERIFICATION = "com.android.statementservice" private const val PNAME_MICROG_SERVICES_CORE = "com.google.android.gms" - val appsCompatibiltyPNames = setOf( + val compatibiltyPNames = setOf( PNAME_PWAPLAYER, PNAME_INTENT_VERIFICATION, PNAME_MICROG_SERVICES_CORE ) } @@ -53,18 +55,22 @@ class AppListsRepository( packageName = "foundation.e.dummysystemapp", uid = -1, label = context.getString(R.string.dummy_system_app_label), - icon = context.getDrawable(R.drawable.ic_e_app_logo) + icon = context.getDrawable(R.drawable.ic_e_app_logo), + profileId = -1, + profileType = ProfileType.MAIN ) - val dummyAppsCompatibilityApp = ApplicationDescription( + val dummyCompatibilityApp = ApplicationDescription( packageName = "foundation.e.dummyappscompatibilityapp", uid = -2, label = context.getString(R.string.dummy_apps_compatibility_app_label), - icon = context.getDrawable(R.drawable.ic_apps_compatibility_components) + icon = context.getDrawable(R.drawable.ic_apps_compatibility_components), + profileId = -1, + profileType = ProfileType.MAIN ) - private suspend fun fetchAppDescriptions() { - val launcherPackageNames = pm.queryIntentActivities( + private suspend fun fetchAppDescriptions(fetchMissingIcons: Boolean = false) { + val launcherPackageNames = context.packageManager.queryIntentActivities( Intent(Intent.ACTION_MAIN, null).apply { addCategory(Intent.CATEGORY_LAUNCHER) }, 0 ).mapNotNull { it.activityInfo?.packageName } @@ -79,104 +85,151 @@ class AppListsRepository( isHiddenSystemApp(packageInfo.applicationInfo, launcherPackageNames) } - val aCFilter = { packageInfo: PackageInfo -> - packageInfo.packageName in appsCompatibiltyPNames + val compatibilityAppsFilter = { packageInfo: PackageInfo -> + packageInfo.packageName in compatibiltyPNames } - val visibleApps = permissionsModule.getApplications(visibleAppsFilter, true) - val hiddenApps = permissionsModule.getApplications(hiddenAppsFilter, false) - val aCApps = permissionsModule.getApplications(aCFilter, false) + val visibleApps = recycleIcons( + newApps = permissionsModule.getApplications(visibleAppsFilter), + fetchMissingIcons = fetchMissingIcons + ) + val hiddenApps = permissionsModule.getApplications(hiddenAppsFilter) + val compatibilityApps = permissionsModule.getApplications(compatibilityAppsFilter) + + updateMaps(visibleApps + hiddenApps + compatibilityApps) + + allProfilesAppDescriptions.emit( + Triple( + visibleApps + dummySystemApp + dummyCompatibilityApp, + hiddenApps, + compatibilityApps + ) + ) + } + + private fun recycleIcons( + newApps: List<ApplicationDescription>, + fetchMissingIcons: Boolean + ): List<ApplicationDescription> { + val oldVisibleApps = allProfilesAppDescriptions.value.first + return newApps.map { app -> + app.copy( + icon = oldVisibleApps.find { app.apId == it.apId }?.icon + ?: if (fetchMissingIcons) permissionsModule.getApplicationIcon(app) else null + ) + } + } - val workProfileVisibleApps = permissionsModule.getWorkProfileApplications(visibleAppsFilter, true) - val workProfileHiddenApps = permissionsModule.getWorkProfileApplications(hiddenAppsFilter, false) - val workProfileACApps = permissionsModule.getApplications(aCFilter, false) + private fun updateMaps(apps: List<ApplicationDescription>) { + val byUid = mutableMapOf<Int, ApplicationDescription>() + val byApId = mutableMapOf<String, ApplicationDescription>() + apps.forEach { app -> + byUid[app.uid]?.run { packageName > app.packageName } == true + if (byUid[app.uid].let { it == null || it.packageName > app.packageName }) { + byUid[app.uid] = app + } - appDescriptions.emit((visibleApps + dummySystemApp + dummyAppsCompatibilityApp) to hiddenApps) - allProfilesAppDescriptions.emit(Triple( - (visibleApps + workProfileVisibleApps + dummySystemApp + dummyAppsCompatibilityApp), - (hiddenApps + workProfileHiddenApps), - (aCApps + workProfileACApps) - )) + byApId[app.apId] = app + } + appsByUid = byUid + appsByAPId = byApId } + private var lastFetchApps = 0 private var refreshAppJob: Job? = null - private fun refreshAppDescriptions() { - if (refreshAppJob != null) { - return - } else { + private fun refreshAppDescriptions(fetchMissingIcons: Boolean = true, force: Boolean = false): Job? { + if (refreshAppJob == null) { refreshAppJob = coroutineScope.launch(Dispatchers.IO) { - fetchAppDescriptions() - refreshAppJob = null + if (force || context.packageManager.getChangedPackages(lastFetchApps) != null) { + fetchAppDescriptions(fetchMissingIcons = fetchMissingIcons) + if (fetchMissingIcons) { + lastFetchApps = context.packageManager.getChangedPackages(lastFetchApps) + ?.sequenceNumber ?: lastFetchApps + } + + refreshAppJob = null + } } } + + return refreshAppJob } - fun getVisibleApps(): Flow<List<ApplicationDescription>> { + fun mainProfileApps(): Flow<List<ApplicationDescription>> { refreshAppDescriptions() - return appDescriptions.map { it.first.sortedBy { app -> app.label.toString().lowercase() } } + return allProfilesAppDescriptions.map { + it.first.filter { app -> app.profileType == ProfileType.MAIN } + .sortedBy { app -> app.label.toString().lowercase() } + } } - fun getHiddenSystemApps(): List<ApplicationDescription> { - return appDescriptions.value.second + fun getMainProfileHiddenSystemApps(): List<ApplicationDescription> { + return allProfilesAppDescriptions.value.second.filter { it.profileType == ProfileType.MAIN } } - fun getAllProfilesVisibleApps(): Flow<List<ApplicationDescription>> { + fun apps(): Flow<List<ApplicationDescription>> { refreshAppDescriptions() - return allProfilesAppDescriptions.map { it.first.sortedBy { app -> app.label.toString().lowercase() } } + return allProfilesAppDescriptions.map { + it.first.sortedBy { app -> app.label.toString().lowercase() } + } + } + + fun allApps(): Flow<List<ApplicationDescription>> { + return allProfilesAppDescriptions.map { + it.first + it.second + it.third + } } - fun getAllProfilesHiddenSystemApps(): List<ApplicationDescription> { + private fun getHiddenSystemApps(): List<ApplicationDescription> { return allProfilesAppDescriptions.value.second } - fun getAllProfilesACApps(): List<ApplicationDescription> { + private fun getCompatibilityApps(): List<ApplicationDescription> { return allProfilesAppDescriptions.value.third } - fun getAllApps(): Flow<List<ApplicationDescription>> = getAllProfilesVisibleApps() - .map { it + getAllProfilesHiddenSystemApps() + getAllProfilesACApps()} - - fun getApplicationDescription(appUid: Int): ApplicationDescription? { - return allProfilesAppDescriptions.value.first.find { it.uid == appUid } + fun anyForHiddenApps(app: ApplicationDescription, test: (ApplicationDescription) -> Boolean): Boolean { + return if (app == dummySystemApp) { + getHiddenSystemApps().any { test(it) } + } else if (app == dummyCompatibilityApp) { + getCompatibilityApps().any { test(it) } + } else test(app) } - fun foldForHiddenApp(appUid: Int, appValueGetter: (Int) -> Int): Int { - return if (appUid == dummySystemApp.uid) { - getAllProfilesHiddenSystemApps().fold(0) { acc, app -> - acc + appValueGetter(app.uid) - } - } else if (appUid == dummyAppsCompatibilityApp.uid) { - getAllProfilesACApps().fold(0) { acc, app -> - acc + appValueGetter(app.uid) - } - } else appValueGetter(appUid) + fun applyForHiddenApps(app: ApplicationDescription, action: (ApplicationDescription) -> Unit) { + mapReduceForHiddenApps(app = app, map = action, reduce = {}) } - fun anyForHiddenApps(appUid: Int, test: (Int) -> Boolean): Boolean { - return if (appUid == dummySystemApp.uid) { - getAllProfilesHiddenSystemApps().any { test(it.uid) } - } else if (appUid == dummyAppsCompatibilityApp.uid) { - getAllProfilesACApps().any { test(it.uid) } - } else test(appUid) + fun <T, R> mapReduceForHiddenApps( + app: ApplicationDescription, + map: (ApplicationDescription) -> T, + reduce: (List<T>) -> R + ): R { + return if (app == dummySystemApp) { + reduce(getHiddenSystemApps().map(map)) + } else if (app == dummyCompatibilityApp) { + reduce(getCompatibilityApps().map(map)) + } else reduce(listOf(map(app))) } - fun applyForHiddenApps(appUid: Int, action: (Int) -> Unit) { - if (appUid == dummySystemApp.uid) { - getAllProfilesHiddenSystemApps().forEach { action(it.uid) } - } else if (appUid == dummyAppsCompatibilityApp.uid) { - getAllProfilesACApps().forEach { action(it.uid) } - } else action(appUid) - } + private var appsByUid = mapOf<Int, ApplicationDescription>() + private var appsByAPId = mapOf<String, ApplicationDescription>() + fun getApp(appUid: Int): ApplicationDescription? { + return appsByUid[appUid] ?: run { + runBlocking { refreshAppDescriptions(fetchMissingIcons = false, force = true)?.join() } + appsByUid[appUid] + } + } - private val pm get() = context.packageManager + fun getApp(apId: String): ApplicationDescription? { + if (apId.isBlank()) return null - private val appDescriptions = MutableStateFlow( - Pair( - emptyList<ApplicationDescription>(), - emptyList<ApplicationDescription>() - ) - ) + return appsByAPId[apId] ?: run { + runBlocking { refreshAppDescriptions(fetchMissingIcons = false, force = true)?.join() } + appsByAPId[apId] + } + } private val allProfilesAppDescriptions = MutableStateFlow( Triple( @@ -209,7 +262,7 @@ class AppListsRepository( private fun isStandardApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { return when { app.packageName == PNAME_SETTINGS -> false - app.packageName in appsCompatibiltyPNames -> false + app.packageName in compatibiltyPNames -> false app.hasFlag(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) -> true !app.hasFlag(ApplicationInfo.FLAG_SYSTEM) -> true launcherApps.contains(app.packageName) -> true @@ -219,7 +272,7 @@ class AppListsRepository( private fun isHiddenSystemApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { return when { - app.packageName in appsCompatibiltyPNames -> false + app.packageName in compatibiltyPNames -> false else -> !isNotHiddenSystemApp(app, launcherApps) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt index 0b76c7b..afdd2d5 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -21,6 +22,7 @@ import android.graphics.drawable.Drawable import foundation.e.privacymodules.permissions.data.ApplicationDescription data class AppWithCounts( + val appDesc: ApplicationDescription, val packageName: String, val uid: Int, var label: CharSequence?, @@ -40,6 +42,7 @@ data class AppWithCounts( leaks: Int, ) : this( + appDesc = app, packageName = app.packageName, uid = app.uid, label = app.label, @@ -52,5 +55,5 @@ data class AppWithCounts( ) val blockedTrackersCount get() = if (isWhitelisted) 0 - else trackersCount - whiteListedTrackersCount + else Math.max(trackersCount - whiteListedTrackersCount, 0) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt index 4821349..dd62839 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt @@ -24,8 +24,16 @@ import kotlinx.coroutines.flow.Flow class AppListUseCase( private val appListsRepository: AppListsRepository ) { - + val dummySystemApp = appListsRepository.dummySystemApp + fun getApp(uid: Int): ApplicationDescription { + return when (uid) { + dummySystemApp.uid -> dummySystemApp + appListsRepository.dummyCompatibilityApp.uid -> + appListsRepository.dummyCompatibilityApp + else -> appListsRepository.getApp(uid) ?: dummySystemApp + } + } fun getAppsUsingInternet(): Flow<List<ApplicationDescription>> { - return appListsRepository.getVisibleApps() + return appListsRepository.mainProfileApps() } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt index caba132..dcb417b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt @@ -17,7 +17,6 @@ package foundation.e.privacycentralapp.domain.usecases -import android.util.Log import foundation.e.privacycentralapp.data.repositories.AppListsRepository import foundation.e.privacycentralapp.data.repositories.LocalStateRepository import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode @@ -86,7 +85,7 @@ class IpScramblingStateUseCase( } private fun getHiddenPackageNames(): List<String> { - return appListsRepository.getHiddenSystemApps().map { it.packageName } + return appListsRepository.getMainProfileHiddenSystemApps().map { it.packageName } } val bypassTorApps: Set<String> get() { @@ -97,10 +96,10 @@ class IpScramblingStateUseCase( mutable.add(appListsRepository.dummySystemApp.packageName) whitelist = mutable } - if (AppListsRepository.appsCompatibiltyPNames.any { it in whitelist }) { + if (AppListsRepository.compatibiltyPNames.any { it in whitelist }) { val mutable = whitelist.toMutableSet() - mutable.removeAll(AppListsRepository.appsCompatibiltyPNames) - mutable.add(appListsRepository.dummyAppsCompatibilityApp.packageName) + mutable.removeAll(AppListsRepository.compatibiltyPNames) + mutable.add(appListsRepository.dummyCompatibilityApp.packageName) whitelist = mutable } return whitelist @@ -113,16 +112,16 @@ class IpScramblingStateUseCase( if (visibleList.contains(packageName)) { if (packageName == appListsRepository.dummySystemApp.packageName) { rawList.removeAll(getHiddenPackageNames()) - } else if (packageName == appListsRepository.dummyAppsCompatibilityApp.packageName) { - rawList.removeAll(AppListsRepository.appsCompatibiltyPNames) + } else if (packageName == appListsRepository.dummyCompatibilityApp.packageName) { + rawList.removeAll(AppListsRepository.compatibiltyPNames) } else { rawList.remove(packageName) } } else { if (packageName == appListsRepository.dummySystemApp.packageName) { rawList.addAll(getHiddenPackageNames()) - } else if (packageName == appListsRepository.dummyAppsCompatibilityApp.packageName) { - rawList.addAll(AppListsRepository.appsCompatibiltyPNames) + } else if (packageName == appListsRepository.dummyCompatibilityApp.packageName) { + rawList.addAll(AppListsRepository.compatibiltyPNames) } else { rawList.add(packageName) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt index 820073b..afb6d1e 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS + * Copyright (C) 2021 E FOUNDATION, 2022 - 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 @@ -36,7 +36,12 @@ class TrackersStateUseCase( private val coroutineScope: CoroutineScope ) { init { - trackersPrivacyModule.start(trackersRepository.trackers, enableNotification = false) + trackersPrivacyModule.start( + trackers = trackersRepository.trackers, + getAppByAPId = appListsRepository::getApp, + getAppByUid = appListsRepository::getApp, + enableNotification = false + ) coroutineScope.launch { localStateRepository.blockTrackers.collect { enabled -> if (enabled) { @@ -54,39 +59,47 @@ class TrackersStateUseCase( blockTrackersPrivacyModule.isWhiteListEmpty() } - fun getApplicationDescription(appUid: Int): ApplicationDescription? { - return appListsRepository.getApplicationDescription(appUid) + fun isWhitelisted(app: ApplicationDescription): Boolean { + return isWhitelisted(app, appListsRepository, blockTrackersPrivacyModule) } - fun isWhitelisted(appUid: Int): Boolean { - return isWhitelisted(appUid, appListsRepository, blockTrackersPrivacyModule) + fun toggleAppWhitelist(app: ApplicationDescription, isWhitelisted: Boolean) { + appListsRepository.applyForHiddenApps(app) { + blockTrackersPrivacyModule.setWhiteListed(it, isWhitelisted) + } + updateAllTrackersBlockedState() } - fun toggleAppWhitelist(appUid: Int, isWhitelisted: Boolean) { - appListsRepository.applyForHiddenApps(appUid) { uid -> - blockTrackersPrivacyModule.setWhiteListed(uid, isWhitelisted) + fun blockTracker(app: ApplicationDescription, tracker: Tracker, isBlocked: Boolean) { + appListsRepository.applyForHiddenApps(app) { + blockTrackersPrivacyModule.setWhiteListed(tracker, it, !isBlocked) } - updateAllTrackersBlockedState() } - fun blockTracker(appUid: Int, tracker: Tracker, isBlocked: Boolean) { - appListsRepository.applyForHiddenApps(appUid) { uid -> - blockTrackersPrivacyModule.setWhiteListed(tracker, uid, !isBlocked) - } + fun clearWhitelist(app: ApplicationDescription) { + appListsRepository.applyForHiddenApps( + app, + blockTrackersPrivacyModule::clearWhiteList + ) updateAllTrackersBlockedState() } fun updateTrackers() = coroutineScope.launch { trackersRepository.update() - trackersPrivacyModule.start(trackersRepository.trackers, enableNotification = false) + trackersPrivacyModule.start( + trackers = trackersRepository.trackers, + getAppByAPId = appListsRepository::getApp, + getAppByUid = appListsRepository::getApp, + enableNotification = false + ) } } fun isWhitelisted( - appUid: Int, + app: ApplicationDescription, appListsRepository: AppListsRepository, blockTrackersPrivacyModule: IBlockTrackersPrivacyModule ): Boolean { - return appListsRepository.anyForHiddenApps(appUid, blockTrackersPrivacyModule::isWhitelisted) + return appListsRepository.anyForHiddenApps(app, blockTrackersPrivacyModule::isWhitelisted) } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt index cc6ec45..5ca7039 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS + * Copyright (C) 2021 E FOUNDATION, 2022 - 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 @@ -47,7 +47,7 @@ class TrackersStatisticsUseCase( private val resources: Resources ) { fun initAppList() { - appListsRepository.getAllProfilesVisibleApps() + appListsRepository.apps() } private fun rawUpdates(): Flow<Unit> = callbackFlow { @@ -74,10 +74,25 @@ class TrackersStatisticsUseCase( ) to trackTrackersPrivacyModule.getTrackersCount() } + fun getNonBlockedTrackersCount(): Flow<Int> { + return if (blockTrackersPrivacyModule.isBlockingEnabled()) + appListsRepository.allApps().map { apps -> + val whiteListedTrackers = mutableSetOf<Tracker>() + val whiteListedApps = blockTrackersPrivacyModule.getWhiteListedApp() + apps.forEach { app -> + if (app in whiteListedApps) { + whiteListedTrackers.addAll(trackTrackersPrivacyModule.getTrackersForApp(app)) + } else { + whiteListedTrackers.addAll(blockTrackersPrivacyModule.getWhiteList(app)) + } + } + whiteListedTrackers.size + } + else flowOf(trackTrackersPrivacyModule.getTrackersCount()) + } + fun getMostLeakedApp(): ApplicationDescription? { - return appListsRepository.getApplicationDescription( - trackTrackersPrivacyModule.getPastDayMostLeakedApp() - ) + return trackTrackersPrivacyModule.getPastDayMostLeakedApp() } fun getDayTrackersCalls() = trackTrackersPrivacyModule.getPastDayTrackersCalls() @@ -161,103 +176,93 @@ class TrackersStatisticsUseCase( } } - fun getTrackers(appUid: Int): List<Tracker> { - val trackers = if (appUid == appListsRepository.dummySystemApp.uid) { - appListsRepository.getAllProfilesHiddenSystemApps().map { - trackTrackersPrivacyModule.getTrackersForApp(it.uid) - }.flatten().distinctBy { it.id } - } else if (appUid == appListsRepository.dummyAppsCompatibilityApp.uid) { - appListsRepository.getAllProfilesACApps().map { - trackTrackersPrivacyModule.getTrackersForApp(it.uid) - }.flatten().distinctBy { it.id } - } else trackTrackersPrivacyModule.getTrackersForApp(appUid) - - return trackers.sortedBy { it.label.lowercase() } - } - - fun getTrackersWithWhiteList(appUid: Int): List<Pair<Tracker, Boolean>> { - val trackers: List<Tracker> - val whiteListedTrackersIds: Set<String> - if (appUid == appListsRepository.dummySystemApp.uid) { - val hiddenApps = appListsRepository.getAllProfilesHiddenSystemApps() - trackers = trackTrackersPrivacyModule.getTrackers(hiddenApps.map { it.uid }) - - whiteListedTrackersIds = hiddenApps.fold(HashSet<String>()) { acc, app -> - acc.addAll(blockTrackersPrivacyModule.getWhiteList(app.uid).map { it.id }) - acc - } - } else if (appUid == appListsRepository.dummyAppsCompatibilityApp.uid) { - val acApps = appListsRepository.getAllProfilesACApps() - trackers = trackTrackersPrivacyModule.getTrackers(acApps.map { it.uid }) + fun getTrackersWithWhiteList(app: ApplicationDescription): List<Pair<Tracker, Boolean>> { + return appListsRepository.mapReduceForHiddenApps( + app = app, + map = { appDesc: ApplicationDescription -> + ( + trackTrackersPrivacyModule.getTrackersForApp(appDesc) to + blockTrackersPrivacyModule.getWhiteList(appDesc) + ) + }, + reduce = { lists -> + lists.unzip().let { (trackerLists, whiteListedIdLists) -> + val whiteListedIds = whiteListedIdLists.flatten().map { it.id }.toSet() - whiteListedTrackersIds = acApps.fold(HashSet<String>()) { acc, app -> - acc.addAll(blockTrackersPrivacyModule.getWhiteList(app.uid).map { it.id }) - acc + trackerLists.flatten().distinctBy { it.id }.sortedBy { it.label.lowercase() } + .map { tracker -> tracker to (tracker.id in whiteListedIds) } + } } - } else { - trackers = trackTrackersPrivacyModule.getTrackersForApp(appUid) - whiteListedTrackersIds = blockTrackersPrivacyModule.getWhiteList(appUid) - .map { it.id }.toSet() - } + ) + } - return trackers.sortedBy { it.label.lowercase() }.map { tracker -> tracker to whiteListedTrackersIds.any { tracker.id == it } } + fun isWhiteListEmpty(app: ApplicationDescription): Boolean { + return appListsRepository.mapReduceForHiddenApps( + app = app, + map = { appDesc: ApplicationDescription -> + blockTrackersPrivacyModule.getWhiteList(appDesc).isEmpty() + }, + reduce = { areEmpty -> areEmpty.all { it } } + ) } - fun getCalls(appUid: Int): Pair<Int, Int> { - return if (appUid == appListsRepository.dummySystemApp.uid) { - appListsRepository.getAllProfilesHiddenSystemApps().map { - trackTrackersPrivacyModule.getPastDayTrackersCallsForApp(it.uid) - }.reduce { (accBlocked, accLeaked), (blocked, leaked) -> - accBlocked + blocked to accLeaked + leaked - } - } else if (appUid == appListsRepository.dummyAppsCompatibilityApp.uid) { - appListsRepository.getAllProfilesACApps().map { - trackTrackersPrivacyModule.getPastDayTrackersCallsForApp(it.uid) - }.reduce { (accBlocked, accLeaked), (blocked, leaked) -> - accBlocked + blocked to accLeaked + leaked + fun getCalls(app: ApplicationDescription): Pair<Int, Int> { + return appListsRepository.mapReduceForHiddenApps( + app = app, + map = trackTrackersPrivacyModule::getPastDayTrackersCallsForApp, + reduce = { zip -> + zip.unzip().let { (blocked, leaked) -> + blocked.sum() to leaked.sum() + } } - } else trackTrackersPrivacyModule.getPastDayTrackersCallsForApp(appUid) + ) } fun getAppsWithCounts(): Flow<List<AppWithCounts>> { val trackersCounts = trackTrackersPrivacyModule.getTrackersCountByApp() val hiddenAppsTrackersWithWhiteList = - getTrackersWithWhiteList(appListsRepository.dummySystemApp.uid) + getTrackersWithWhiteList(appListsRepository.dummySystemApp) val acAppsTrackersWithWhiteList = - getTrackersWithWhiteList(appListsRepository.dummyAppsCompatibilityApp.uid) + getTrackersWithWhiteList(appListsRepository.dummyCompatibilityApp) - return appListsRepository.getAllProfilesVisibleApps() + return appListsRepository.apps() .map { apps -> val callsByApp = trackTrackersPrivacyModule.getPastDayTrackersCallsByApps() apps.map { app -> + val calls = appListsRepository.mapReduceForHiddenApps( + app = app, + map = { callsByApp.getOrDefault(app, 0 to 0) }, + reduce = { + it.unzip().let { (blocked, leaked) -> + blocked.sum() to leaked.sum() + } + } + ) + AppWithCounts( app = app, isWhitelisted = !blockTrackersPrivacyModule.isBlockingEnabled() || - isWhitelisted(app.uid, appListsRepository, blockTrackersPrivacyModule), - trackersCount = if (app.uid == appListsRepository.dummySystemApp.uid) { - hiddenAppsTrackersWithWhiteList.size - } else if (app.uid == appListsRepository.dummyAppsCompatibilityApp.uid) { - acAppsTrackersWithWhiteList.size - } else { - trackersCounts.getOrDefault(app.uid, 0) - }, - whiteListedTrackersCount = if (app.uid == appListsRepository.dummySystemApp.uid) { - hiddenAppsTrackersWithWhiteList.count { it.second } - } else if (app.uid == appListsRepository.dummyAppsCompatibilityApp.uid) { - acAppsTrackersWithWhiteList.count { it.second } - } else { - blockTrackersPrivacyModule.getWhiteList(app.uid).size + isWhitelisted(app, appListsRepository, blockTrackersPrivacyModule), + trackersCount = when (app) { + appListsRepository.dummySystemApp -> + hiddenAppsTrackersWithWhiteList.size + appListsRepository.dummyCompatibilityApp -> + acAppsTrackersWithWhiteList.size + else -> trackersCounts.getOrDefault(app, 0) }, - blockedLeaks = appListsRepository.foldForHiddenApp(app.uid) { - appUid -> - callsByApp.getOrDefault(appUid, 0 to 0).first + whiteListedTrackersCount = when (app) { + appListsRepository.dummySystemApp -> + hiddenAppsTrackersWithWhiteList.count { it.second } + appListsRepository.dummyCompatibilityApp -> + acAppsTrackersWithWhiteList.count { it.second } + else -> + blockTrackersPrivacyModule.getWhiteList(app).size }, - leaks = appListsRepository.foldForHiddenApp(app.uid) { - appUid -> - callsByApp.getOrDefault(appUid, 0 to 0).second - } + blockedLeaks = calls.first, + leaks = calls.second ) - }.sortedWith(mostLeakedAppsComparator) + } + .sortedWith(mostLeakedAppsComparator) } } @@ -270,21 +275,4 @@ class TrackersStatisticsUseCase( } } } - - fun getNonBlockedTrackersCount(): Flow<Int> { - return if (blockTrackersPrivacyModule.isBlockingEnabled()) - appListsRepository.getAllApps().map { apps -> - val whiteListedTrackers = mutableSetOf<Tracker>() - val whiteListedAppUids = blockTrackersPrivacyModule.getWhiteListedApp() - apps.forEach { app -> - if (app.uid in whiteListedAppUids) { - whiteListedTrackers.addAll(getTrackers(app.uid)) - } else { - whiteListedTrackers.addAll(blockTrackersPrivacyModule.getWhiteList(app.uid)) - } - } - whiteListedTrackers.size - } - else flowOf(trackTrackersPrivacyModule.getTrackersCount()) - } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt index ead01a5..f3a9774 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt @@ -1,4 +1,5 @@ /* +* Copyright (C) 2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt index f15119e..888c140 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -85,6 +86,9 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) { binding.blockAllToggle.setOnClickListener { viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked)) } + binding.btnReset.setOnClickListener { + viewModel.submitAction(AppTrackersViewModel.Action.ResetAllTrackers) + } binding.trackers.apply { layoutManager = LinearLayoutManager(requireContext()) @@ -94,7 +98,7 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) { onToggleSwitch = { tracker, isBlocked -> viewModel.submitAction(AppTrackersViewModel.Action.ToggleTrackerAction(tracker, isBlocked)) }, - onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) } + onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) }, ) } @@ -162,15 +166,19 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) { ) } binding.noTrackersYet.isVisible = false + binding.btnReset.isVisible = true } else { binding.trackersListTitle.isVisible = false binding.trackers.isVisible = false binding.noTrackersYet.isVisible = true binding.noTrackersYet.text = getString( - if (state.isBlockingActivated) - R.string.apptrackers_no_trackers_yet_block_on - else R.string.apptrackers_no_trackers_yet_block_off + when { + !state.isBlockingActivated -> R.string.apptrackers_no_trackers_yet_block_off + state.isWhitelistEmpty -> R.string.apptrackers_no_trackers_yet_block_on + else -> R.string.app_trackers_no_trackers_yet_remaining_whitelist + } ) + binding.btnReset.isVisible = state.isBlockingActivated && !state.isWhitelistEmpty } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt index 8088443..a190a74 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -27,6 +28,7 @@ data class AppTrackersState( val leaked: Int = 0, val blocked: Int = 0, val isTrackersBlockingEnabled: Boolean = false, + val isWhitelistEmpty: Boolean = true, val showQuickPrivacyDisabledMessage: Boolean = false, ) { fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? { diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt index 1a33844..e5a94f9 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -25,6 +26,7 @@ import foundation.e.privacycentralapp.domain.entities.TrackerMode import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.api.Tracker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -38,7 +40,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class AppTrackersViewModel( - private val appUid: Int, + private val app: ApplicationDescription, private val trackersStateUseCase: TrackersStateUseCase, private val trackersStatisticsUseCase: TrackersStatisticsUseCase, private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase @@ -57,9 +59,12 @@ class AppTrackersViewModel( viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( - appDesc = trackersStateUseCase.getApplicationDescription(appUid), - isBlockingActivated = !trackersStateUseCase.isWhitelisted(appUid), - trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(appUid), + appDesc = app, + isBlockingActivated = !trackersStateUseCase.isWhitelisted(app), + trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList( + app + ), + isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app) ) } } @@ -79,6 +84,7 @@ class AppTrackersViewModel( is Action.BlockAllToggleAction -> blockAllToggleAction(action) is Action.ToggleTrackerAction -> toggleTrackerAction(action) is Action.ClickTracker -> actionClickTracker(action) + is Action.ResetAllTrackers -> resetAllTrackers() } } @@ -87,10 +93,10 @@ class AppTrackersViewModel( if (!state.value.isTrackersBlockingEnabled) { _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled) } - trackersStateUseCase.toggleAppWhitelist(appUid, !action.isBlocked) + trackersStateUseCase.toggleAppWhitelist(app, !action.isBlocked) _state.update { it.copy( - isBlockingActivated = !trackersStateUseCase.isWhitelisted(appUid) + isBlockingActivated = !trackersStateUseCase.isWhitelisted(app) ) } } @@ -103,14 +109,8 @@ class AppTrackersViewModel( } if (state.value.isBlockingActivated) { - trackersStateUseCase.blockTracker(appUid, action.tracker, action.isBlocked) - _state.update { - it.copy( - trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList( - appUid - ) - ) - } + trackersStateUseCase.blockTracker(app, action.tracker, action.isBlocked) + updateWhitelist() } } } @@ -130,13 +130,29 @@ class AppTrackersViewModel( } } + private suspend fun resetAllTrackers() { + withContext(Dispatchers.IO) { + trackersStateUseCase.clearWhitelist(app) + updateWhitelist() + } + } private fun fetchStatistics() { - val (blocked, leaked) = trackersStatisticsUseCase.getCalls(appUid) + val (blocked, leaked) = trackersStatisticsUseCase.getCalls(app) return _state.update { s -> s.copy( - trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(appUid), + trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app), leaked = leaked, blocked = blocked, + isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app) + ) + } + } + + private fun updateWhitelist() { + _state.update { s -> + s.copy( + trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app), + isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app) ) } } @@ -151,5 +167,6 @@ class AppTrackersViewModel( data class BlockAllToggleAction(val isBlocked: Boolean) : Action() data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action() data class ClickTracker(val tracker: Tracker) : Action() + object ResetAllTrackers : Action() } } diff --git a/app/src/main/res/layout/apptrackers_fragment.xml b/app/src/main/res/layout/apptrackers_fragment.xml index e6e226f..d0a72d5 100644 --- a/app/src/main/res/layout/apptrackers_fragment.xml +++ b/app/src/main/res/layout/apptrackers_fragment.xml @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2023 MURENA SAS ~ Copyright (C) 2021 E FOUNDATION ~ ~ This program is free software: you can redistribute it and/or modify @@ -82,20 +83,33 @@ /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/trackers" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_width="match_parent" - android:layout_marginBottom="32dp" + android:layout_marginBottom="16dp" tools:listitem="@layout/apptrackers_item_tracker_toggle" android:visibility="gone" /> <TextView android:id="@+id/no_trackers_yet" - android:padding="32dp" + android:paddingHorizontal="32dp" + android:paddingTop="64dp" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:gravity="center" tools:text="@string/apptrackers_no_trackers_yet_block_off" /> + <com.google.android.material.button.MaterialButton + android:id="@+id/btn_reset" + style="@style/Widget.MaterialComponents.Button.OutlinedButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/apptrackers_reset" + android:textSize="14sp" + android:layout_gravity="center" + android:visibility="gone" + tools:visibility="visible" + android:layout_margin="16dp" + /> </LinearLayout> </androidx.core.widget.NestedScrollView> </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38cd38b..49d9182 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,10 +92,12 @@ <string name="apptrackers_trackers_list_title">Opt for the trackers you want to activate/deactivate.</string> <string name="apptrackers_no_trackers_yet_block_off">No trackers were detected yet. If new trackers are detected they will be updated here.</string> <string name="apptrackers_no_trackers_yet_block_on">No trackers were detected yet. All future trackers will be blocked.</string> + <string name="app_trackers_no_trackers_yet_remaining_whitelist">No trackers were detected yet. Some trackers were unblocked previously.</string> <string name="apptrackers_error_quickprivacy_disabled">Enable Quick Privacy to be able to activate/deactivate trackers.</string> <string name="apptrackers_trackers_count_summary">%1$d blocked trackers out of %2$d detected trackers, %3$d blocked leaks and %4$d allowed leaks.</string> <string name="apptrackers_error_no_app">App not installed.</string> <string name="apptrackers_tracker_control_disabled_message">Changes will take effect when tracker blocker is on.</string> + <string name="apptrackers_reset">Reset trackers</string> <!-- Features warning dialog --> <string name="warningdialog_do_not_show_again">Do not show again</string> diff --git a/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt b/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt index 42cf658..cf6fb2c 100644 --- a/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt +++ b/fakelocation/fakelocationdemo/src/main/java/foundation/e/privacymodules/fakelocationdemo/MainActivity.kt @@ -38,6 +38,7 @@ import foundation.e.privacymodules.fakelocationdemo.databinding.ActivityMainBind import foundation.e.privacymodules.permissions.PermissionsPrivacyModule import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.permissions.data.ProfileType class MainActivity : AppCompatActivity() { companion object { @@ -54,7 +55,9 @@ class MainActivity : AppCompatActivity() { packageName = packageName, uid = myUid(), label = getString(R.string.app_name), - icon = null + icon = null, + profileId = 0, + profileType = ProfileType.MAIN ) } diff --git a/permissionse/libs/hidden-apis-stub/src/main/java/android/content/pm/PackageManager.java b/permissionse/libs/hidden-apis-stub/src/main/java/android/content/pm/PackageManager.java index 3f232bf..c6232ce 100644 --- a/permissionse/libs/hidden-apis-stub/src/main/java/android/content/pm/PackageManager.java +++ b/permissionse/libs/hidden-apis-stub/src/main/java/android/content/pm/PackageManager.java @@ -26,11 +26,26 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresPermission; import java.util.List; +import android.util.AndroidException; // Stub based on: // https://gitlab.e.foundation/e/os/android_frameworks_base/-/blob/[SDK_VERSION]/core/java/android/content/pm/PackageManager.java public abstract class PackageManager { + @TargetApi(29) + @DeprecatedSinceApi( + api = 34, + message = "Check availability in SDK34" + ) + public static class NameNotFoundException extends AndroidException { + public NameNotFoundException() { + } + + public NameNotFoundException(String name) { + super(name); + } + } + @TargetApi(29) @DeprecatedSinceApi( @@ -58,6 +73,18 @@ public abstract class PackageManager { @TargetApi(29) @DeprecatedSinceApi( + api = 33, + message = "@deprecated Use {@link #getApplicationInfoAsUser(String, ApplicationInfoFlags, int)} instead." + ) + public abstract ApplicationInfo getApplicationInfoAsUser( + @NonNull String packageName, + int flags, + int userId + ) throws NameNotFoundException; + + + @TargetApi(29) + @DeprecatedSinceApi( api = 34, message = "Check disponibility in SDK34" ) diff --git a/permissionse/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt b/permissionse/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt index cde4afb..6d0a17c 100644 --- a/permissionse/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt +++ b/permissionse/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt @@ -26,6 +26,7 @@ import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.UserInfo +import android.graphics.drawable.Drawable import android.net.IConnectivityManager import android.net.VpnManager import android.net.VpnManager.TYPE_VPN_SERVICE @@ -36,6 +37,8 @@ import android.os.UserManager import android.util.Log import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription +import foundation.e.privacymodules.permissions.data.ProfileType.MAIN +import foundation.e.privacymodules.permissions.data.ProfileType.WORK /** * Implements [IPermissionsPrivacyModule] with all privileges of a system app. @@ -88,6 +91,50 @@ class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(con return true } + override fun getApplications( + filter: ((PackageInfo) -> Boolean)? + ): List<ApplicationDescription> { + val pm = context.packageManager + val mainUserId = UserHandle.myUserId() + val workProfileId = getWorkProfile()?.id + + val userIds = listOf(mainUserId, workProfileId).filterNotNull() + return userIds.map { profileId -> + pm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, profileId) + .filter { filter?.invoke(it) ?: true } + .map { + buildApplicationDescription( + appInfo = it.applicationInfo, + profileId = profileId, + profileType = if (profileId == mainUserId) MAIN else WORK + ) + } + }.flatten() + } + + override fun getApplicationIcon(app: ApplicationDescription): Drawable? { + return if (app.profileType == WORK) { + getWorkProfile()?.let { workProfile -> + val pm = context.packageManager + getApplicationIcon( + pm.getApplicationInfoAsUser(app.packageName, 0, workProfile.id) + )?.let { + pm.getUserBadgedIcon(it, workProfile.getUserHandle()) + } + } + } else getApplicationIcon(app.packageName) + } + + override fun setBlockable(notificationChannel: NotificationChannel) { + when (Build.VERSION.SDK_INT) { + 29 -> notificationChannel.setBlockableSystem(true) + 30, 31, 32, 33 -> notificationChannel.setBlockable(true) + else -> { + Log.e("Permissions-e", "Bad android sdk version") + } + } + } + override fun setVpnPackageAuthorization(packageName: String): Boolean { return when (Build.VERSION.SDK_INT) { 29 -> setVpnPackageAuthorizationSDK29(packageName) @@ -161,45 +208,6 @@ class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(con return false } - private fun getWorkProfile(): UserInfo? { - val userManager: UserManager = context.getSystemService(UserManager::class.java) - val userId = UserHandle.myUserId() - for (user in userManager.getProfiles(UserHandle.myUserId())) { - if (user.id != userId && userManager.isManagedProfile(user.id)) { - return user - } - } - return null - } - - override fun getApplications( - filter: ((PackageInfo) -> Boolean)?, - withIcon: Boolean - ): List<ApplicationDescription> { - return context.packageManager - .getInstalledPackages(PackageManager.GET_PERMISSIONS) - .filter { filter?.invoke(it) ?: true } - .map { buildApplicationDescription(it.applicationInfo, withIcon = withIcon) } - } - - override fun getWorkProfileApplications( - filter: ((PackageInfo) -> Boolean)?, - withIcon: Boolean - ): List<ApplicationDescription> { - val pm = context.packageManager - return getWorkProfile()?.let { workProfile -> - pm.getInstalledPackagesAsUser(PackageManager.GET_PERMISSIONS, workProfile.id) - .filter { filter?.invoke(it) ?: true } - .map { - val appDesc = buildApplicationDescription(it.applicationInfo, withIcon = withIcon) - appDesc.icon = appDesc.icon?.let { - pm.getUserBadgedIcon(it, workProfile.getUserHandle()) - } - appDesc - } - } ?: emptyList() - } - override fun getAlwaysOnVpnPackage(): String? { return when (Build.VERSION.SDK_INT) { 29, 30 -> getAlwaysOnVpnPackageSDK29() @@ -236,13 +244,14 @@ class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(con } } - override fun setBlockable(notificationChannel: NotificationChannel) { - when (Build.VERSION.SDK_INT) { - 29 -> notificationChannel.setBlockableSystem(true) - 30, 31, 32, 33 -> notificationChannel.setBlockable(true) - else -> { - Log.e("Permissions-e", "Bad android sdk version") + private fun getWorkProfile(): UserInfo? { + val userManager: UserManager = context.getSystemService(UserManager::class.java) + val userId = UserHandle.myUserId() + for (user in userManager.getProfiles(UserHandle.myUserId())) { + if (user.id != userId && userManager.isManagedProfile(user.id)) { + return user } } + return null } } diff --git a/permissionsstandalone/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt b/permissionsstandalone/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt index da7c73e..283b417 100644 --- a/permissionsstandalone/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt +++ b/permissionsstandalone/src/main/java/foundation/e/privacymodules/permissions/PermissionsPrivacyModule.kt @@ -21,6 +21,7 @@ import android.app.NotificationChannel import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.graphics.drawable.Drawable import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription @@ -29,19 +30,15 @@ import foundation.e.privacymodules.permissions.data.ApplicationDescription */ class PermissionsPrivacyModule(context: Context) : APermissionsPrivacyModule(context) { override fun getApplications( - filter: ((PackageInfo) -> Boolean)?, - withIcon: Boolean + filter: ((PackageInfo) -> Boolean)? ): List<ApplicationDescription> { return context.packageManager.getInstalledPackages(PackageManager.GET_PERMISSIONS) .filter { filter?.invoke(it) == true } - .map { buildApplicationDescription(it.applicationInfo, withIcon = withIcon) } + .map { buildApplicationDescription(it.applicationInfo) } } - override fun getWorkProfileApplications( - filter: ((PackageInfo) -> Boolean)?, - withIcon: Boolean - ): List<ApplicationDescription> { - return emptyList() + override fun getApplicationIcon(app: ApplicationDescription): Drawable? { + return getApplicationIcon(app.packageName) } /** diff --git a/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/APermissionsPrivacyModule.kt b/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/APermissionsPrivacyModule.kt index d0e2e75..64b2292 100644 --- a/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/APermissionsPrivacyModule.kt +++ b/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/APermissionsPrivacyModule.kt @@ -1,5 +1,6 @@ /* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS + * Copyright (C) 2022 - 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 @@ -29,6 +30,7 @@ import android.util.Log import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.permissions.data.PermissionDescription +import foundation.e.privacymodules.permissions.data.ProfileType /** * Implementation of the commons functionality between privileged and standard @@ -40,28 +42,12 @@ abstract class APermissionsPrivacyModule(protected val context: Context) : IPerm companion object { private const val TAG = "PermissionsModule" } - /** - * @see IPermissionsPrivacyModule.getAllApplications - */ - override fun getAllApplications(): List<ApplicationDescription> { - val appInfos = context.packageManager.getInstalledApplications(0) - return appInfos.map { buildApplicationDescription(it, false) } - } - - /** - * @see IPermissionsPrivacyModule.getInstalledApplications - */ - override fun getInstalledApplications(): List<ApplicationDescription> { - return context.packageManager.getInstalledApplications(0) - .filter { it.flags and ApplicationInfo.FLAG_SYSTEM == 0 } - .map { buildApplicationDescription(it, false) } - } /** * @see IPermissionsPrivacyModule.getInstalledApplications */ override fun getApplicationDescription(packageName: String, withIcon: Boolean): ApplicationDescription { - val appDesc = buildApplicationDescription(context.packageManager.getApplicationInfo(packageName, 0), false) + val appDesc = buildApplicationDescription(context.packageManager.getApplicationInfo(packageName, 0)) if (withIcon) { appDesc.icon = getApplicationIcon(appDesc.packageName) } @@ -144,13 +130,19 @@ abstract class APermissionsPrivacyModule(protected val context: Context) : IPerm } } - override fun buildApplicationDescription(appInfo: ApplicationInfo, withIcon: Boolean): + override fun buildApplicationDescription( + appInfo: ApplicationInfo, + profileId: Int, + profileType: ProfileType + ): ApplicationDescription { return ApplicationDescription( packageName = appInfo.packageName, uid = appInfo.uid, label = getAppLabel(appInfo), - icon = if (withIcon) getApplicationIcon(appInfo) else null + icon = null, + profileId = profileId, + profileType = profileType, ) } @@ -158,7 +150,7 @@ abstract class APermissionsPrivacyModule(protected val context: Context) : IPerm return context.packageManager.getApplicationLabel(appInfo) } - private fun getApplicationIcon(appInfo: ApplicationInfo): Drawable? { + fun getApplicationIcon(appInfo: ApplicationInfo): Drawable? { return context.packageManager.getApplicationIcon(appInfo) } diff --git a/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/IPermissionsPrivacyModule.kt b/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/IPermissionsPrivacyModule.kt index b64762f..39c726a 100644 --- a/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/IPermissionsPrivacyModule.kt +++ b/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/IPermissionsPrivacyModule.kt @@ -1,5 +1,6 @@ /* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS + * Copyright (C) 2022 - 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 @@ -24,6 +25,7 @@ import android.graphics.drawable.Drawable import foundation.e.privacymodules.permissions.data.AppOpModes import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.permissions.data.PermissionDescription +import foundation.e.privacymodules.permissions.data.ProfileType /** * List applications and manage theirs permissions. @@ -32,32 +34,15 @@ interface IPermissionsPrivacyModule { fun buildApplicationDescription( appInfo: ApplicationInfo, - withIcon: Boolean = true + profileId: Int = -1, + profileType: ProfileType = ProfileType.MAIN ): ApplicationDescription fun getApplications( filter: ((PackageInfo) -> Boolean)?, - withIcon: Boolean - ): List<ApplicationDescription> - - fun getWorkProfileApplications( - filter: ((PackageInfo) -> Boolean)?, - withIcon: Boolean ): List<ApplicationDescription> /** - * List the installed application on the device which have not the FLAGS_SYSTEM. - * @return list of filled up [ApplicationDescription] - */ - fun getInstalledApplications(): List<ApplicationDescription> - - /** - * List all the installed application on the device. - * @return list of filled up [ApplicationDescription] - */ - fun getAllApplications(): List<ApplicationDescription> - - /** * List of permissions names used by an app, specified by its [packageName]. * @param packageName the appId of the app * @return the list off permission, in the "android.permission.PERMISSION" format. @@ -131,6 +116,11 @@ interface IPermissionsPrivacyModule { fun getApplicationIcon(packageName: String): Drawable? /** + * Get the application icon. + */ + fun getApplicationIcon(app: ApplicationDescription): Drawable? + + /** * Authorize the specified package to be used as Vpn. * @return true if authorization has been set, false if an error has occurred. */ diff --git a/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/data/ApplicationDescription.kt b/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/data/ApplicationDescription.kt index cafe256..4fa1bb9 100644 --- a/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/data/ApplicationDescription.kt +++ b/privacymodule-api/src/main/java/foundation/e/privacymodules/permissions/data/ApplicationDescription.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -25,6 +26,25 @@ import android.graphics.drawable.Drawable data class ApplicationDescription( val packageName: String, val uid: Int, + val profileId: Int, + val profileType: ProfileType, var label: CharSequence?, var icon: Drawable? -) +) { + val profileFlag = when (profileType) { + ProfileType.MAIN -> PROFILE_FLAG_MAIN + ProfileType.WORK -> PROFILE_FLAG_WORK + else -> profileId + } + + val apId: String get() = "${profileFlag}_$packageName" + + companion object { + const val PROFILE_FLAG_MAIN = -1 + const val PROFILE_FLAG_WORK = -2 + } +} + +enum class ProfileType { + MAIN, WORK, OTHER +} diff --git a/trackers/build.gradle b/trackers/build.gradle index f888acf..ecf95be 100644 --- a/trackers/build.gradle +++ b/trackers/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation( Libs.Kotlin.stdlib, Libs.AndroidX.coreKtx, - Libs.Coroutines.core + Libs.Coroutines.core, + Libs.timber ) } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt index 737aa4a..44793a4 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/DNSBlockerRunnable.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -135,7 +136,6 @@ class DNSBlockerRunnable( private fun shouldBlock(appUid: Int, trackerId: String?): Boolean { return whitelistRepository.isBlockingEnabled && - !whitelistRepository.isAppWhiteListed(appUid) && - !whitelistRepository.isTrackerWhiteListedForApp(trackerId, appUid) + !whitelistRepository.isWhiteListed(appUid, trackerId) } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt index 99e2148..f3c4745 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/TrackersLogger.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -39,8 +40,8 @@ class TrackersLogger(context: Context) { stopped = true } - fun logAccess(trackerId: String?, appId: Int, wasBlocked: Boolean) { - queue.offer(DetectedTracker(trackerId, appId, wasBlocked)) + fun logAccess(trackerId: String?, appUid: Int, wasBlocked: Boolean) { + queue.offer(DetectedTracker(trackerId, appUid, wasBlocked)) } private fun startWriteLogLoop() { diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt index 25f0f2a..7463b22 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/BlockTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -18,6 +19,7 @@ package foundation.e.privacymodules.trackers.api import android.content.Context +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.data.TrackersRepository import foundation.e.privacymodules.trackers.data.WhitelistRepository @@ -52,14 +54,14 @@ class BlockTrackersPrivacyModule(context: Context) : IBlockTrackersPrivacyModule mListeners.forEach { listener -> listener.onBlockingToggle(true) } } - override fun getWhiteList(appUid: Int): List<Tracker> { - return whitelistRepository.getWhiteListForApp(appUid).mapNotNull { + override fun getWhiteList(app: ApplicationDescription): List<Tracker> { + return whitelistRepository.getWhiteListForApp(app).mapNotNull { trackersRepository.getTracker(it) } } - override fun getWhiteListedApp(): List<Int> { - return whitelistRepository.whiteListedApp + override fun getWhiteListedApp(): List<ApplicationDescription> { + return whitelistRepository.getWhiteListedApp() } override fun isBlockingEnabled(): Boolean { @@ -70,19 +72,27 @@ class BlockTrackersPrivacyModule(context: Context) : IBlockTrackersPrivacyModule return whitelistRepository.areWhiteListEmpty() } - override fun isWhitelisted(appUid: Int): Boolean { - return whitelistRepository.isAppWhiteListed(appUid) + override fun isWhitelisted(app: ApplicationDescription): Boolean { + return whitelistRepository.isAppWhiteListed(app) } override fun removeListener(listener: IBlockTrackersPrivacyModule.Listener) { mListeners.remove(listener) } - override fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) { - whitelistRepository.setWhiteListed(tracker, appUid, isWhiteListed) + override fun setWhiteListed( + tracker: Tracker, + app: ApplicationDescription, + isWhiteListed: Boolean + ) { + whitelistRepository.setWhiteListed(tracker, app.apId, isWhiteListed) } - override fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) { - whitelistRepository.setWhiteListed(appUid, isWhiteListed) + override fun setWhiteListed(app: ApplicationDescription, isWhiteListed: Boolean) { + whitelistRepository.setWhiteListed(app.apId, isWhiteListed) + } + + override fun clearWhiteList(app: ApplicationDescription) { + whitelistRepository.clearWhiteList(app.apId) } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt index 9e1a041..3547b8e 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/IBlockTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -17,6 +18,8 @@ package foundation.e.privacymodules.trackers.api +import foundation.e.privacymodules.permissions.data.ApplicationDescription + /** * Manage trackers blocking and whitelisting. */ @@ -40,18 +43,18 @@ interface IBlockTrackersPrivacyModule { /** * Set or unset in whitelist the App with the specified uid. - * @param appUid the uid of the app + * @param app the ApplicationDescription of the app * @param isWhiteListed true, the app will appears in whitelist, false, it won't */ - fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) + fun setWhiteListed(app: ApplicationDescription, isWhiteListed: Boolean) /** * Set or unset in whitelist the specifid tracked, for the App specified by its uid. * @param tracker the tracker - * @param appUid the uid of the app + * @param app the ApplicationDescription of the app * @param isWhiteListed true, the app will appears in whitelist, false, it won't */ - fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) + fun setWhiteListed(tracker: Tracker, app: ApplicationDescription, isWhiteListed: Boolean) /** * Return true if nothing has been added to the whitelist : everything is blocked. @@ -61,17 +64,19 @@ interface IBlockTrackersPrivacyModule { /** * Return the white listed App, by their UID */ - fun getWhiteListedApp(): List<Int> + fun getWhiteListedApp(): List<ApplicationDescription> /** * Return true if the App is whitelisted for trackers blocking. */ - fun isWhitelisted(appUid: Int): Boolean + fun isWhitelisted(app: ApplicationDescription): Boolean /** * List the white listed trackers for an App specified by it uid */ - fun getWhiteList(appUid: Int): List<Tracker> + fun getWhiteList(app: ApplicationDescription): List<Tracker> + + fun clearWhiteList(app: ApplicationDescription) /** * Callback interface to get updates about the state of the Block trackers module. diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt index 264f247..8aaed4a 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/ITrackTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -17,34 +18,41 @@ package foundation.e.privacymodules.trackers.api +import foundation.e.privacymodules.permissions.data.ApplicationDescription + /** * Get reporting about trackers calls. */ interface ITrackTrackersPrivacyModule { - fun start(trackers: List<Tracker>, enableNotification: Boolean = true) + fun start( + trackers: List<Tracker>, + getAppByUid: (Int) -> ApplicationDescription?, + getAppByAPId: (String) -> ApplicationDescription?, + enableNotification: Boolean = true + ) /** * List all the trackers encountered for a specific app. */ - fun getTrackersForApp(appUid: Int): List<Tracker> + fun getTrackersForApp(app: ApplicationDescription): List<Tracker> /** * List all the trackers encountere trackers since "ever", for the given [appUids], * or all apps if [appUids] is null */ - fun getTrackers(appUids: List<Int>? = null): List<Tracker> + fun getTrackers(apps: List<ApplicationDescription>? = null): List<Tracker> /** * Return the number of encountered trackers since "ever", for the given [appUids], * or all apps if [appUids] is null */ - fun getTrackersCount(appUids: List<Int>? = null): Int + fun getTrackersCount(): Int /** * Return the number of encountere trackers since "ever", for each app uid. */ - fun getTrackersCountByApp(): Map<Int, Int> + fun getTrackersCountByApp(): Map<ApplicationDescription, Int> /** * Return the number of encountered trackers for the last 24 hours @@ -79,11 +87,11 @@ interface ITrackTrackersPrivacyModule { */ fun getPastYearTrackersCalls(): List<Pair<Int, Int>> - fun getPastDayTrackersCallsByApps(): Map<Int, Pair<Int, Int>> + fun getPastDayTrackersCallsByApps(): Map<ApplicationDescription, Pair<Int, Int>> - fun getPastDayTrackersCallsForApp(appUid: Int): Pair<Int, Int> + fun getPastDayTrackersCallsForApp(app: ApplicationDescription): Pair<Int, Int> - fun getPastDayMostLeakedApp(): Int + fun getPastDayMostLeakedApp(): ApplicationDescription? interface Listener { diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt index 18c56c9..5fc5b6b 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/api/TrackTrackersPrivacyModule.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -19,9 +20,11 @@ package foundation.e.privacymodules.trackers.api import android.content.Context import android.content.Intent +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.DNSBlockerService import foundation.e.privacymodules.trackers.data.StatsRepository import foundation.e.privacymodules.trackers.data.TrackersRepository +import foundation.e.privacymodules.trackers.data.WhitelistRepository import java.time.temporal.ChronoUnit class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersPrivacyModule { @@ -42,8 +45,15 @@ class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersP } } - override fun start(trackers: List<Tracker>, enableNotification: Boolean) { + override fun start( + trackers: List<Tracker>, + getAppByUid: (Int) -> ApplicationDescription?, + getAppByAPId: (String) -> ApplicationDescription?, + enableNotification: Boolean + ) { TrackersRepository.getInstance().setTrackersList(trackers) + StatsRepository.getInstance(context).setAppGetters(getAppByUid, getAppByAPId) + WhitelistRepository.getInstance(context).setAppGetters(context, getAppByAPId, getAppByUid) val intent = Intent(context, DNSBlockerService::class.java) intent.action = DNSBlockerService.ACTION_START intent.putExtra(DNSBlockerService.EXTRA_ENABLE_NOTIFICATION, enableNotification) @@ -62,20 +72,20 @@ class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersP return statsRepository.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS) } - override fun getTrackersCount(appUids: List<Int>?): Int { - return statsRepository.getContactedTrackersCount(appUids) + override fun getTrackersCount(): Int { + return statsRepository.getContactedTrackersCount() } - override fun getTrackersCountByApp(): Map<Int, Int> { + override fun getTrackersCountByApp(): Map<ApplicationDescription, Int> { return statsRepository.getContactedTrackersCountByApp() } - override fun getTrackersForApp(appUid: Int): List<Tracker> { - return statsRepository.getTrackers(listOf(appUid)) + override fun getTrackersForApp(app: ApplicationDescription): List<Tracker> { + return statsRepository.getTrackers(listOf(app)) } - override fun getTrackers(appUids: List<Int>?): List<Tracker> { - return statsRepository.getTrackers(appUids) + override fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> { + return statsRepository.getTrackers(apps) } override fun getPastDayTrackersCount(): Int { @@ -90,16 +100,16 @@ class TrackTrackersPrivacyModule(private val context: Context) : ITrackTrackersP return statsRepository.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS) } - override fun getPastDayMostLeakedApp(): Int { + override fun getPastDayMostLeakedApp(): ApplicationDescription? { return statsRepository.getMostLeakedApp(24, ChronoUnit.HOURS) } - override fun getPastDayTrackersCallsByApps(): Map<Int, Pair<Int, Int>> { + override fun getPastDayTrackersCallsByApps(): Map<ApplicationDescription, Pair<Int, Int>> { return statsRepository.getCallsByApps(24, ChronoUnit.HOURS) } - override fun getPastDayTrackersCallsForApp(appUid: Int): Pair<Int, Int> { - return statsRepository.getCalls(appUid, 24, ChronoUnit.HOURS) + override fun getPastDayTrackersCallsForApp(app: ApplicationDescription): Pair<Int, Int> { + return statsRepository.getCalls(app, 24, ChronoUnit.HOURS) } override fun addListener(listener: ITrackTrackersPrivacyModule.Listener) { diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt index 21edb56..4d287d4 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsDatabase.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -23,13 +24,15 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.provider.BaseColumns +import androidx.core.database.getStringOrNull import foundation.e.privacymodules.trackers.api.Tracker -import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APP_UID +import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APPID import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_BLOCKED import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_CONTACTED import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TIMESTAMP import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TRACKER import foundation.e.privacymodules.trackers.data.StatsDatabase.AppTrackerEntry.TABLE_NAME +import timber.log.Timber import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -40,38 +43,44 @@ class StatsDatabase(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { companion object { - const val DATABASE_VERSION = 1 + const val DATABASE_VERSION = 2 const val DATABASE_NAME = "TrackerFilterStats.db" private const val SQL_CREATE_TABLE = "CREATE TABLE $TABLE_NAME (" + "${BaseColumns._ID} INTEGER PRIMARY KEY," + "$COLUMN_NAME_TIMESTAMP INTEGER," + - "$COLUMN_NAME_APP_UID INTEGER," + "$COLUMN_NAME_TRACKER TEXT," + "$COLUMN_NAME_NUMBER_CONTACTED INTEGER," + - "$COLUMN_NAME_NUMBER_BLOCKED INTEGER)" + "$COLUMN_NAME_NUMBER_BLOCKED INTEGER," + + "$COLUMN_NAME_APPID TEXT)" private const val PROJECTION_NAME_PERIOD = "period" private const val PROJECTION_NAME_CONTACTED_SUM = "contactedsum" private const val PROJECTION_NAME_BLOCKED_SUM = "blockedsum" private const val PROJECTION_NAME_LEAKED_SUM = "leakedsum" private const val PROJECTION_NAME_TRACKERS_COUNT = "trackerscount" + + private val MIGRATE_1_2 = listOf( + "ALTER TABLE $TABLE_NAME ADD COLUMN $COLUMN_NAME_APPID TEXT" + // "ALTER TABLE $TABLE_NAME DROP COLUMN app_uid" + // DROP COLUMN is available since sqlite 3.35.0, and sdk29 as 3.22.0, sdk32 as 3.32.2 + ) } object AppTrackerEntry : BaseColumns { const val TABLE_NAME = "tracker_filter_stats" const val COLUMN_NAME_TIMESTAMP = "timestamp" const val COLUMN_NAME_TRACKER = "tracker" - const val COLUMN_NAME_APP_UID = "app_uid" const val COLUMN_NAME_NUMBER_CONTACTED = "sum_contacted" const val COLUMN_NAME_NUMBER_BLOCKED = "sum_blocked" + const val COLUMN_NAME_APPID = "app_apid" } private var projection = arrayOf( COLUMN_NAME_TIMESTAMP, - COLUMN_NAME_APP_UID, COLUMN_NAME_TRACKER, COLUMN_NAME_NUMBER_CONTACTED, - COLUMN_NAME_NUMBER_BLOCKED + COLUMN_NAME_NUMBER_BLOCKED, + COLUMN_NAME_APPID ) private val lock = Any() @@ -82,7 +91,13 @@ class StatsDatabase(context: Context) : } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - onCreate(db) + if (oldVersion == 1 && newVersion == 2) { + MIGRATE_1_2.forEach(db::execSQL) + } else { + Timber.e( + "Unexpected database versions: oldVersion: $oldVersion ; newVersion: $newVersion" + ) + } } private fun getCallsByPeriod( @@ -128,11 +143,11 @@ class StatsDatabase(context: Context) : javaPeriodFormat: String ): List<Pair<Int, Int>> { var currentDate = ZonedDateTime.now().minus(periodsCount.toLong(), periodUnit) - val formater = DateTimeFormatter.ofPattern(javaPeriodFormat) + val formatter = DateTimeFormatter.ofPattern(javaPeriodFormat) val calls = mutableListOf<Pair<Int, Int>>() for (i in 0 until periodsCount) { currentDate = currentDate.plus(1, periodUnit) - val currentPeriod = formater.format(currentDate) + val currentPeriod = formatter.format(currentDate) calls.add(callsByPeriod.getOrDefault(currentPeriod, 0 to 0)) } return calls @@ -182,15 +197,11 @@ class StatsDatabase(context: Context) : } } - fun getContactedTrackersCount(appUids: List<Int>?): Int { + fun getContactedTrackersCount(): Int { synchronized(lock) { val db = readableDatabase var query = "SELECT DISTINCT $COLUMN_NAME_TRACKER FROM $TABLE_NAME" - appUids?.let { - query += " WHERE $COLUMN_NAME_APP_UID IN (${it.joinToString(", ")})" - } - val cursor = db.rawQuery(query, arrayOf()) var count = 0 while (cursor.moveToNext()) { @@ -204,19 +215,19 @@ class StatsDatabase(context: Context) : } } - fun getContactedTrackersCountByApp(): Map<Int, Int> { + fun getContactedTrackersCountByAppId(): Map<String, Int> { synchronized(lock) { val db = readableDatabase - val projection = "$COLUMN_NAME_APP_UID, $COLUMN_NAME_TRACKER" + val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER" val cursor = db.rawQuery( "SELECT DISTINCT $projection FROM $TABLE_NAME", // + arrayOf() ) - val countByApp = mutableMapOf<Int, Int>() + val countByApp = mutableMapOf<String, Int>() while (cursor.moveToNext()) { trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { - val appUid = cursor.getInt(COLUMN_NAME_APP_UID) - countByApp[appUid] = countByApp.getOrDefault(appUid, 0) + 1 + val appId = cursor.getString(COLUMN_NAME_APPID) + countByApp[appId] = countByApp.getOrDefault(appId, 0) + 1 } } cursor.close() @@ -225,26 +236,26 @@ class StatsDatabase(context: Context) : } } - fun getCallsByApps(periodCount: Int, periodUnit: TemporalUnit): Map<Int, Pair<Int, Int>> { + fun getCallsByAppIds(periodCount: Int, periodUnit: TemporalUnit): Map<String, Pair<Int, Int>> { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase val selection = "$COLUMN_NAME_TIMESTAMP >= ?" val selectionArg = arrayOf("" + minTimestamp) - val projection = "$COLUMN_NAME_APP_UID, " + + val projection = "$COLUMN_NAME_APPID, " + "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," + "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM" val cursor = db.rawQuery( "SELECT $projection FROM $TABLE_NAME" + " WHERE $selection" + - " GROUP BY $COLUMN_NAME_APP_UID", + " GROUP BY $COLUMN_NAME_APPID", selectionArg ) - val callsByApp = HashMap<Int, Pair<Int, Int>>() + val callsByApp = HashMap<String, Pair<Int, Int>>() while (cursor.moveToNext()) { val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) - callsByApp[cursor.getInt(COLUMN_NAME_APP_UID)] = blocked to contacted - blocked + callsByApp[cursor.getString(COLUMN_NAME_APPID)] = blocked to contacted - blocked } cursor.close() db.close() @@ -252,13 +263,13 @@ class StatsDatabase(context: Context) : } } - fun getCalls(appUid: Int, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { + fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase - val selection = "$COLUMN_NAME_APP_UID = ? AND " + + val selection = "$COLUMN_NAME_APPID = ? AND " + "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + appUid, "" + minTimestamp) + val selectionArg = arrayOf("" + appId, "" + minTimestamp) val projection = "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," + "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM" @@ -278,37 +289,37 @@ class StatsDatabase(context: Context) : } } - fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): Int { + fun getMostLeakedAppId(periodCount: Int, periodUnit: TemporalUnit): String { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase val selection = "$COLUMN_NAME_TIMESTAMP >= ?" val selectionArg = arrayOf("" + minTimestamp) - val projection = "$COLUMN_NAME_APP_UID, " + + val projection = "$COLUMN_NAME_APPID, " + "SUM($COLUMN_NAME_NUMBER_CONTACTED - $COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_LEAKED_SUM" val cursor = db.rawQuery( "SELECT $projection FROM $TABLE_NAME" + " WHERE $selection" + - " GROUP BY $COLUMN_NAME_APP_UID" + + " GROUP BY $COLUMN_NAME_APPID" + " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1", selectionArg ) - var appUid = 0 + var appId = "" if (cursor.moveToNext()) { - appUid = cursor.getInt(COLUMN_NAME_APP_UID) + appId = cursor.getString(COLUMN_NAME_APPID) } cursor.close() db.close() - return appUid + return appId } } - fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { + fun logAccess(trackerId: String?, appId: String, blocked: Boolean) { synchronized(lock) { val currentHour = getCurrentHourTs() val db = writableDatabase val values = ContentValues() - values.put(COLUMN_NAME_APP_UID, appUid) + values.put(COLUMN_NAME_APPID, appId) values.put(COLUMN_NAME_TRACKER, trackerId) values.put(COLUMN_NAME_TIMESTAMP, currentHour) @@ -317,9 +328,9 @@ class StatsDatabase(context: Context) : query+=COLUMN_NAME_NUMBER_BLOCKED+" = "+COLUMN_NAME_NUMBER_BLOCKED+" + 1 "; */ val selection = "$COLUMN_NAME_TIMESTAMP = ? AND " + - "$COLUMN_NAME_APP_UID = ? AND " + + "$COLUMN_NAME_APPID = ? AND " + "$COLUMN_NAME_TRACKER = ? " - val selectionArg = arrayOf("" + currentHour, "" + appUid, trackerId) + val selectionArg = arrayOf("" + currentHour, "" + appId, trackerId) val cursor = db.query( TABLE_NAME, projection, @@ -355,23 +366,22 @@ class StatsDatabase(context: Context) : private fun cursorToEntry(cursor: Cursor): StatEntry { val entry = StatEntry() - entry.timestamp = - cursor.getLong(COLUMN_NAME_TIMESTAMP) - entry.app_uid = cursor.getInt(COLUMN_NAME_APP_UID) + entry.timestamp = cursor.getLong(COLUMN_NAME_TIMESTAMP) + entry.appId = cursor.getString(COLUMN_NAME_APPID) entry.sum_blocked = cursor.getInt(COLUMN_NAME_NUMBER_BLOCKED) entry.sum_contacted = cursor.getInt(COLUMN_NAME_NUMBER_CONTACTED) entry.tracker = cursor.getInt(COLUMN_NAME_TRACKER) return entry } - fun getTrackers(appUids: List<Int>?): List<Tracker> { + fun getTrackers(appIds: List<String>?): List<Tracker> { synchronized(lock) { - val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APP_UID) + val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID) var selection: String? = null var selectionArg: Array<String>? = null - appUids?.let { - selection = "$COLUMN_NAME_APP_UID IN (${it.joinToString(", ")})" + appIds?.let { appIds -> + selection = "$COLUMN_NAME_APPID IN (${appIds.joinToString(", ") { "'$it'" }})" selectionArg = arrayOf() } @@ -402,7 +412,7 @@ class StatsDatabase(context: Context) : } class StatEntry { - var app_uid = 0 + var appId = "" var sum_contacted = 0 var sum_blocked = 0 var timestamp: Long = 0 @@ -442,6 +452,8 @@ class StatsDatabase(context: Context) : private fun Cursor.getString(columnName: String): String { val columnIndex = getColumnIndex(columnName) - return if (columnIndex >= 0) getString(columnIndex) else "" + return if (columnIndex >= 0) { + getStringOrNull(columnIndex) ?: "" + } else "" } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt index 16d8ec6..8f02adb 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/StatsRepository.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -18,12 +19,15 @@ package foundation.e.privacymodules.trackers.data import android.content.Context +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.api.Tracker import java.time.temporal.TemporalUnit class StatsRepository private constructor(context: Context) { private val database: StatsDatabase private var newDataCallback: (() -> Unit)? = null + private var getAppByUid: ((Int) -> ApplicationDescription?)? = null + private var getAppByAPId: ((String) -> ApplicationDescription?)? = null companion object { private var instance: StatsRepository? = null @@ -32,6 +36,14 @@ class StatsRepository private constructor(context: Context) { } } + fun setAppGetters( + getAppByUid: (Int) -> ApplicationDescription?, + getAppByAPId: (String) -> ApplicationDescription? + ) { + this.getAppByUid = getAppByUid + this.getAppByAPId = getAppByAPId + } + init { database = StatsDatabase(context) } @@ -41,8 +53,10 @@ class StatsRepository private constructor(context: Context) { } fun logAccess(trackerId: String?, appUid: Int, blocked: Boolean) { - database.logAccess(trackerId, appUid, blocked) - newDataCallback?.invoke() + getAppByUid?.invoke(appUid)?.let { app -> + database.logAccess(trackerId, app.apId, blocked) + newDataCallback?.invoke() + } } fun getTrackersCallsOnPeriod( @@ -56,27 +70,36 @@ class StatsRepository private constructor(context: Context) { return database.getActiveTrackersByPeriod(periodsCount, periodUnit) } - fun getContactedTrackersCountByApp(): Map<Int, Int> { - return database.getContactedTrackersCountByApp() + fun getContactedTrackersCountByApp(): Map<ApplicationDescription, Int> { + return database.getContactedTrackersCountByAppId().mapByAppIdToApp() } - fun getContactedTrackersCount(appUids: List<Int>?): Int { - return database.getContactedTrackersCount(appUids) + fun getContactedTrackersCount(): Int { + return database.getContactedTrackersCount() } - fun getTrackers(appUids: List<Int>?): List<Tracker> { - return database.getTrackers(appUids) + fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> { + return database.getTrackers(apps?.map { it.apId }) + } + + fun getCallsByApps( + periodCount: Int, + periodUnit: TemporalUnit + ): Map<ApplicationDescription, Pair<Int, Int>> { + return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp() } - fun getCallsByApps(periodCount: Int, periodUnit: TemporalUnit): Map<Int, Pair<Int, Int>> { - return database.getCallsByApps(periodCount, periodUnit) + fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { + return database.getCalls(app.apId, periodCount, periodUnit) } - fun getCalls(appUid: Int, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> { - return database.getCalls(appUid, periodCount, periodUnit) + fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): ApplicationDescription? { + return getAppByAPId?.invoke(database.getMostLeakedAppId(periodCount, periodUnit)) } - fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): Int { - return database.getMostLeakedApp(periodCount, periodUnit) + private fun <K> Map<String, K>.mapByAppIdToApp(): Map<ApplicationDescription, K> { + return entries.mapNotNull { (apId, value) -> + getAppByAPId?.invoke(apId)?.let { it to value } + }.toMap() } } diff --git a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt index e9f049d..2763d06 100644 --- a/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt +++ b/trackers/src/main/java/foundation/e/privacymodules/trackers/data/WhitelistRepository.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -19,18 +20,28 @@ package foundation.e.privacymodules.trackers.data import android.content.Context import android.content.SharedPreferences +import foundation.e.privacymodules.permissions.data.ApplicationDescription import foundation.e.privacymodules.trackers.api.Tracker +import java.io.File class WhitelistRepository private constructor(context: Context) { - private lateinit var appsWhitelist: Set<Int> - private val trackersWhitelistByApp: MutableMap<Int, MutableSet<String>> = HashMap() + private var appsWhitelist: Set<String> = HashSet() + private var appUidsWhitelist: Set<Int> = HashSet() + + private var trackersWhitelistByApp: MutableMap<String, MutableSet<String>> = HashMap() + private var trackersWhitelistByUid: Map<Int, MutableSet<String>> = HashMap() + private val prefs: SharedPreferences + private var getAppByAPId: ((String) -> ApplicationDescription?)? = null companion object { - private const val SHARED_PREFS_FILE = "trackers_whitelist.prefs" - private const val KEY_BLOKING_ENABLED = "blocking_enabled" + private const val SHARED_PREFS_FILE = "trackers_whitelist_v2" + private const val KEY_BLOCKING_ENABLED = "blocking_enabled" private const val KEY_APPS_WHITELIST = "apps_whitelist" private const val KEY_APP_TRACKERS_WHITELIST_PREFIX = "app_trackers_whitelist_" + + private const val SHARED_PREFS_FILE_V1 = "trackers_whitelist.prefs" + private var instance: WhitelistRepository? = null fun getInstance(context: Context): WhitelistRepository { return instance ?: WhitelistRepository(context).apply { instance = this } @@ -42,83 +53,155 @@ class WhitelistRepository private constructor(context: Context) { reloadCache() } + fun setAppGetters( + context: Context, + getAppByAPId: (String) -> ApplicationDescription?, + getAppByUid: (Int) -> ApplicationDescription? + ) { + this.getAppByAPId = getAppByAPId + migrate(context, getAppByUid) + } + + private fun migrate(context: Context, getAppByUid: (Int) -> ApplicationDescription?) { + if (context.sharedPreferencesExists(SHARED_PREFS_FILE_V1)) { + migrate1To2(context, getAppByUid) + } + } + + private fun Context.sharedPreferencesExists(fileName: String): Boolean { + return File( + "${applicationInfo.dataDir}/shared_prefs/$fileName.xml" + ).exists() + } + + private fun migrate1To2(context: Context, getAppByUid: (Int) -> ApplicationDescription?) { + val prefsV1 = context.getSharedPreferences(SHARED_PREFS_FILE_V1, Context.MODE_PRIVATE) + val editorV2 = prefs.edit() + + editorV2.putBoolean(KEY_BLOCKING_ENABLED, prefsV1.getBoolean(KEY_BLOCKING_ENABLED, false)) + + val apIds = prefsV1.getStringSet(KEY_APPS_WHITELIST, HashSet())?.mapNotNull { + try { + val uid = it.toInt() + getAppByUid(uid)?.apId + } catch (e: Exception) { null } + }?.toSet() ?: HashSet() + + editorV2.putStringSet(KEY_APPS_WHITELIST, apIds) + + prefsV1.all.keys.forEach { key -> + if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) { + try { + val uid = key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length).toInt() + val apId = getAppByUid(uid)?.apId + apId?.let { + val trackers = prefsV1.getStringSet(key, emptySet()) + editorV2.putStringSet(buildAppTrackersKey(apId), trackers) + } + } catch (e: Exception) { } + } + } + editorV2.commit() + + context.deleteSharedPreferences(SHARED_PREFS_FILE_V1) + + reloadCache() + } + private fun reloadCache() { - isBlockingEnabled = prefs.getBoolean(KEY_BLOKING_ENABLED, false) + isBlockingEnabled = prefs.getBoolean(KEY_BLOCKING_ENABLED, false) reloadAppsWhiteList() reloadAllAppTrackersWhiteList() } private fun reloadAppsWhiteList() { - appsWhitelist = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet())?.mapNotNull { - try { it.toInt() } catch (e: Exception) { null } - }?.toHashSet() ?: HashSet() + appsWhitelist = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet()) ?: HashSet() + appUidsWhitelist = appsWhitelist + .mapNotNull { apId -> getAppByAPId?.invoke(apId)?.uid } + .toSet() } - private fun reloadAppTrackersWhiteList(appUid: Int) { - val key = buildAppTrackersKey(appUid) - trackersWhitelistByApp[appUid] = prefs.getStringSet(key, HashSet()) ?: HashSet() + private fun refreshAppUidTrackersWhiteList() { + trackersWhitelistByUid = trackersWhitelistByApp.mapNotNull { (apId, value) -> + getAppByAPId?.invoke(apId)?.uid?.let { uid -> + uid to value + } + }.toMap() } - private fun reloadAllAppTrackersWhiteList() { - trackersWhitelistByApp.clear() + val map: MutableMap<String, MutableSet<String>> = HashMap() prefs.all.keys.forEach { key -> if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) { - val appUid = key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length).toInt() - reloadAppTrackersWhiteList(appUid) + map[key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length)] = ( + prefs.getStringSet(key, HashSet()) ?: HashSet() + ) } } + trackersWhitelistByApp = map } var isBlockingEnabled: Boolean = false get() = field set(enabled) { - prefs.edit().putBoolean(KEY_BLOKING_ENABLED, enabled).apply() + prefs.edit().putBoolean(KEY_BLOCKING_ENABLED, enabled).apply() field = enabled } - fun setWhiteListed(appUid: Int, isWhiteListed: Boolean) { + fun setWhiteListed(apId: String, isWhiteListed: Boolean) { val current = prefs.getStringSet(KEY_APPS_WHITELIST, HashSet())?.toHashSet() ?: HashSet() if (isWhiteListed) { - current.add("" + appUid) + current.add(apId) } else { - current.remove("" + appUid) + current.remove(apId) } prefs.edit().putStringSet(KEY_APPS_WHITELIST, current).commit() reloadAppsWhiteList() } - private fun buildAppTrackersKey(appUid: Int): String { - return KEY_APP_TRACKERS_WHITELIST_PREFIX + appUid + private fun buildAppTrackersKey(apId: String): String { + return KEY_APP_TRACKERS_WHITELIST_PREFIX + apId } - fun setWhiteListed(tracker: Tracker, appUid: Int, isWhiteListed: Boolean) { - val trackers = trackersWhitelistByApp.getOrDefault(appUid, HashSet()) - trackersWhitelistByApp[appUid] = trackers + fun setWhiteListed(tracker: Tracker, apId: String, isWhiteListed: Boolean) { + val trackers = trackersWhitelistByApp.getOrDefault(apId, HashSet()) + trackersWhitelistByApp[apId] = trackers if (isWhiteListed) { trackers.add(tracker.id) } else { trackers.remove(tracker.id) } - prefs.edit().putStringSet(buildAppTrackersKey(appUid), trackers).commit() + refreshAppUidTrackersWhiteList() + prefs.edit().putStringSet(buildAppTrackersKey(apId), trackers).commit() } - fun isAppWhiteListed(appUid: Int): Boolean { - return appsWhitelist.contains(appUid) + fun isAppWhiteListed(app: ApplicationDescription): Boolean { + return appsWhitelist.contains(app.apId) } - fun isTrackerWhiteListedForApp(trackerId: String?, appUid: Int): Boolean { - return trackersWhitelistByApp.getOrDefault(appUid, HashSet()).contains(trackerId) + fun isWhiteListed(appUid: Int, trackerId: String?): Boolean { + return appUidsWhitelist.contains(appUid) || + trackersWhitelistByUid.getOrDefault(appUid, HashSet()).contains(trackerId) } fun areWhiteListEmpty(): Boolean { return appsWhitelist.isEmpty() && trackersWhitelistByApp.all { (_, trackers) -> trackers.isEmpty() } } - val whiteListedApp: List<Int> get() = appsWhitelist.toList() + fun getWhiteListedApp(): List<ApplicationDescription> { + return getAppByAPId?.let { + appsWhitelist.mapNotNull(it) + } ?: emptyList() + } + + fun getWhiteListForApp(app: ApplicationDescription): List<String> { + return trackersWhitelistByApp[app.apId]?.toList() ?: emptyList() + } - fun getWhiteListForApp(appUid: Int): List<String> { - return trackersWhitelistByApp[appUid]?.toList() ?: emptyList() + fun clearWhiteList(apId: String) { + trackersWhitelistByApp.remove(apId) + refreshAppUidTrackersWhiteList() + prefs.edit().remove(buildAppTrackersKey(apId)).commit() } } |