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 /app/src | |
parent | 2df577ca97a674a4bd3875dc5137bb44df2c03ef (diff) |
6556: add AdvancedPrivacy App Id in trackers stats to avoid appUid aliasing
Diffstat (limited to 'app/src')
14 files changed, 342 insertions, 230 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> |