summaryrefslogtreecommitdiff
path: root/app/src/main/java/foundation/e
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/foundation/e')
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt14
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt6
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt197
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt5
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt12
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt17
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt47
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt182
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt1
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt16
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt2
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt49
12 files changed, 322 insertions, 226 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()
}
}