From 2e897cc8af4234abc4e3f5c3448e1fd7b2b8a1bd Mon Sep 17 00:00:00 2001
From: Guillaume Jacquart <guillaume.jacquart@hoodbrains.com>
Date: Tue, 5 Dec 2023 08:17:01 +0000
Subject: 1203 trackers oriented view

---
 .../foundation/e/advancedprivacy/KoinModule.kt     |  27 +++-
 .../e/advancedprivacy/common/AppsAdapter.kt        |  71 ---------
 .../common/extensions/ViewPager2Extensions.kt      |  43 ++++++
 .../domain/usecases/AppTrackersUseCase.kt          |  83 ++++++++++
 .../domain/usecases/TrackerDetailsUseCase.kt       |  55 +++++++
 .../domain/usecases/TrackersAndAppsListsUseCase.kt |  78 ++++++++++
 .../domain/usecases/TrackersStateUseCase.kt        |  20 +--
 .../domain/usecases/TrackersStatisticsUseCase.kt   |  86 +----------
 .../features/trackers/AppsAdapter.kt               |  62 ++++++++
 .../features/trackers/ListsTabPagerAdapter.kt      | 123 +++++++++++++++
 .../features/trackers/TrackerControlDisclaimer.kt  |  81 ++++++++++
 .../features/trackers/TrackersAdapter.kt           |  64 ++++++++
 .../features/trackers/TrackersFragment.kt          | 172 +++++++++++++--------
 .../features/trackers/TrackersState.kt             |  17 +-
 .../features/trackers/TrackersViewModel.kt         |  67 ++++----
 .../trackers/apptrackers/AppTrackersFragment.kt    | 152 +++++++++---------
 .../trackers/apptrackers/AppTrackersState.kt       |  12 +-
 .../trackers/apptrackers/AppTrackersViewModel.kt   | 102 +++++-------
 .../trackers/apptrackers/ToggleTrackersAdapter.kt  |  72 ++++-----
 .../trackers/trackerdetails/TrackerAppsAdapter.kt  |  67 ++++++++
 .../trackerdetails/TrackerDetailsFragment.kt       | 149 ++++++++++++++++++
 .../trackers/trackerdetails/TrackerDetailsState.kt |  31 ++++
 .../trackerdetails/TrackerDetailsViewModel.kt      | 128 +++++++++++++++
 23 files changed, 1311 insertions(+), 451 deletions(-)
 delete mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/extensions/ViewPager2Extensions.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppTrackersUseCase.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackerDetailsUseCase.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt
 create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt

(limited to 'app/src/main/java/foundation/e')

diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt
index efcd096..55183e9 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt
@@ -27,10 +27,13 @@ import foundation.e.advancedprivacy.domain.entities.NotificationContent
 import foundation.e.advancedprivacy.domain.entities.ProfileType
 import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository
 import foundation.e.advancedprivacy.domain.usecases.AppListUseCase
+import foundation.e.advancedprivacy.domain.usecases.AppTrackersUseCase
 import foundation.e.advancedprivacy.domain.usecases.FakeLocationStateUseCase
 import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
 import foundation.e.advancedprivacy.domain.usecases.IpScramblingStateUseCase
 import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackerDetailsUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase
 import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
 import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
 import foundation.e.advancedprivacy.dummy.CityDataSource
@@ -41,8 +44,11 @@ import foundation.e.advancedprivacy.features.internetprivacy.InternetPrivacyView
 import foundation.e.advancedprivacy.features.location.FakeLocationViewModel
 import foundation.e.advancedprivacy.features.trackers.TrackersViewModel
 import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersViewModel
+import foundation.e.advancedprivacy.features.trackers.trackerdetails.TrackerDetailsViewModel
 import foundation.e.advancedprivacy.ipscrambler.ipScramblerModule
 import foundation.e.advancedprivacy.permissions.externalinterfaces.PermissionsPrivacyModuleImpl
+import foundation.e.advancedprivacy.trackers.data.TrackersRepository
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
 import foundation.e.advancedprivacy.trackers.service.trackerServiceModule
 import foundation.e.advancedprivacy.trackers.trackersModule
 import org.koin.android.ext.koin.androidContext
@@ -131,6 +137,10 @@ val appModule = module {
     singleOf(::ShowFeaturesWarningUseCase)
     singleOf(::TrackersStateUseCase)
     singleOf(::TrackersStatisticsUseCase)
+    singleOf(::TrackersAndAppsListsUseCase)
+
+    singleOf(::AppTrackersUseCase)
+    singleOf(::TrackerDetailsUseCase)
 
     single<IPermissionsPrivacyModule> {
         PermissionsPrivacyModuleImpl(context = androidContext())
@@ -144,9 +154,24 @@ val appModule = module {
             app = app,
             trackersStateUseCase = get(),
             trackersStatisticsUseCase = get(),
-            getQuickPrivacyStateUseCase = get()
+            getQuickPrivacyStateUseCase = get(),
+            appTrackersUseCase = get()
         )
     }
+
+    viewModel { parameters ->
+        val trackersRepository: TrackersRepository = get()
+        val tracker = trackersRepository.getTracker(parameters.get()) ?: Tracker("-1", emptySet(), "dummy", null)
+
+        TrackerDetailsViewModel(
+            tracker = tracker,
+            trackersStateUseCase = get(),
+            trackersStatisticsUseCase = get(),
+            getQuickPrivacyStateUseCase = get(),
+            trackerDetailsUseCase = get()
+        )
+    }
+
     viewModelOf(::TrackersViewModel)
     viewModelOf(::FakeLocationViewModel)
     viewModelOf(::InternetPrivacyViewModel)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
deleted file mode 100644
index aee1890..0000000
--- a/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-package foundation.e.advancedprivacy.common
-
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import androidx.recyclerview.widget.RecyclerView
-import foundation.e.advancedprivacy.R
-import foundation.e.advancedprivacy.domain.entities.AppWithCounts
-
-class AppsAdapter(
-    private val itemsLayout: Int,
-    private val listener: (Int) -> Unit
-) :
-    RecyclerView.Adapter<AppsAdapter.ViewHolder>() {
-
-    class ViewHolder(view: View, private val listener: (Int) -> Unit) : RecyclerView.ViewHolder(view) {
-        val appName: TextView = view.findViewById(R.id.title)
-        val counts: TextView = view.findViewById(R.id.counts)
-        val icon: ImageView = view.findViewById(R.id.icon)
-        fun bind(item: AppWithCounts) {
-            appName.text = item.label
-            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) }
-        }
-    }
-
-    var dataSet: List<AppWithCounts> = emptyList()
-        set(value) {
-            field = value
-            notifyDataSetChanged()
-        }
-
-    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-        val view = LayoutInflater.from(parent.context)
-            .inflate(itemsLayout, parent, false)
-        return ViewHolder(view, listener)
-    }
-
-    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
-        val app = dataSet[position]
-        holder.bind(app)
-    }
-
-    override fun getItemCount(): Int = dataSet.size
-}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/extensions/ViewPager2Extensions.kt b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/ViewPager2Extensions.kt
new file mode 100644
index 0000000..e17d692
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/ViewPager2Extensions.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package foundation.e.advancedprivacy.common.extensions
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+
+fun ViewPager2.findViewHolderForAdapterPosition(position: Int): RecyclerView.ViewHolder? {
+    return (getChildAt(0) as RecyclerView).findViewHolderForAdapterPosition(position)
+}
+
+fun ViewPager2.updatePagerHeightForChild(itemView: View) {
+    itemView.post {
+        val wMeasureSpec =
+            View.MeasureSpec.makeMeasureSpec(itemView.width, View.MeasureSpec.EXACTLY)
+        val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+        itemView.measure(wMeasureSpec, hMeasureSpec)
+
+        if (layoutParams.height != itemView.measuredHeight) {
+            layoutParams = (layoutParams)
+                .also { lp ->
+                    // applying Fragment Root View Height to
+                    // the pager LayoutParams, so they match
+                    lp.height = itemView.measuredHeight
+                }
+        }
+    }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppTrackersUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppTrackersUseCase.kt
new file mode 100644
index 0000000..92550ab
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppTrackersUseCase.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package foundation.e.advancedprivacy.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.trackers.data.StatsDatabase
+import foundation.e.advancedprivacy.trackers.data.TrackersRepository
+import foundation.e.advancedprivacy.trackers.data.WhitelistRepository
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase
+
+class AppTrackersUseCase(
+    private val whitelistRepository: WhitelistRepository,
+    private val trackersStateUseCase: TrackersStateUseCase,
+    private val appListsRepository: AppListsRepository,
+    private val statsDatabase: StatsDatabase,
+    private val trackersRepository: TrackersRepository,
+    private val filterHostnameUseCase: FilterHostnameUseCase,
+) {
+    suspend fun toggleAppWhitelist(app: ApplicationDescription, isBlocked: Boolean) {
+        appListsRepository.applyForHiddenApps(app) {
+            whitelistRepository.setWhiteListed(it.apId, !isBlocked)
+            val trackerIds = statsDatabase.getTrackerIds(listOf(app.apId))
+            whitelistRepository.setWhitelistedTrackersForApp(it.apId, trackerIds, !isBlocked)
+        }
+        trackersStateUseCase.updateAllTrackersBlockedState()
+    }
+
+    suspend fun clearWhitelist(app: ApplicationDescription) {
+        appListsRepository.applyForHiddenApps(
+            app
+        ) {
+            whitelistRepository.clearWhiteList(it.apId)
+        }
+        trackersStateUseCase.updateAllTrackersBlockedState()
+    }
+
+    suspend fun getCalls(app: ApplicationDescription): Pair<Int, Int> {
+        return appListsRepository.mapReduceForHiddenApps(
+            app = app,
+            map = {
+                statsDatabase.getCallsForApp(app.apId)
+            },
+            reduce = { zip ->
+                zip.unzip().let { (blocked, leaked) ->
+                    blocked.sum() to leaked.sum()
+                }
+            }
+        )
+    }
+
+    suspend fun getTrackersWithBlockedList(app: ApplicationDescription): List<Pair<Tracker, Boolean>> {
+        val realApIds = appListsRepository.getRealApps(app).map { it.apId }
+        val trackers = statsDatabase.getTrackerIds(realApIds)
+            .mapNotNull { trackersRepository.getTracker(it) }
+
+        return enrichWithBlockedState(app, trackers)
+    }
+
+    suspend fun enrichWithBlockedState(app: ApplicationDescription, trackers: List<Tracker>): List<Pair<Tracker, Boolean>> {
+        val realAppUids = appListsRepository.getRealApps(app).map { it.uid }
+        return trackers.map { tracker ->
+            tracker to !realAppUids.any { uid ->
+                filterHostnameUseCase.isWhitelisted(uid, tracker.id)
+            }
+        }.sortedBy { it.first.label.lowercase() }
+    }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackerDetailsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackerDetailsUseCase.kt
new file mode 100644
index 0000000..27f3e78
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackerDetailsUseCase.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package foundation.e.advancedprivacy.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.trackers.data.StatsDatabase
+import foundation.e.advancedprivacy.trackers.data.WhitelistRepository
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase
+
+class TrackerDetailsUseCase(
+    private val whitelistRepository: WhitelistRepository,
+    private val trackersStateUseCase: TrackersStateUseCase,
+    private val appListsRepository: AppListsRepository,
+    private val statsDatabase: StatsDatabase,
+    private val filterHostnameUseCase: FilterHostnameUseCase,
+) {
+    suspend fun toggleTrackerWhitelist(tracker: Tracker, isBlocked: Boolean) {
+        whitelistRepository.setWhiteListed(tracker, !isBlocked)
+        whitelistRepository.setWhitelistedAppsForTracker(statsDatabase.getApIds(tracker.id), tracker.id, !isBlocked)
+        trackersStateUseCase.updateAllTrackersBlockedState()
+    }
+
+    suspend fun getAppsWithBlockedState(tracker: Tracker): List<Pair<ApplicationDescription, Boolean>> {
+        return enrichWithBlockedState(
+            statsDatabase.getApIds(tracker.id).mapNotNull {
+                appListsRepository.getDisplayableApp(it)
+            }.sortedBy { it.label?.toString() },
+            tracker
+        )
+    }
+
+    suspend fun enrichWithBlockedState(apps: List<ApplicationDescription>, tracker: Tracker): List<Pair<ApplicationDescription, Boolean>> {
+        return apps.map { it to !filterHostnameUseCase.isWhitelisted(it.uid, tracker.id) }
+    }
+
+    suspend fun getCalls(tracker: Tracker): Pair<Int, Int> {
+        return statsDatabase.getCallsForTracker(tracker.id)
+    }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt
new file mode 100644
index 0000000..8292a6d
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package foundation.e.advancedprivacy.domain.usecases
+
+import foundation.e.advancedprivacy.data.repositories.AppListsRepository
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.features.trackers.AppWithTrackersCount
+import foundation.e.advancedprivacy.features.trackers.TrackerWithAppsCount
+import foundation.e.advancedprivacy.trackers.data.StatsDatabase
+import foundation.e.advancedprivacy.trackers.data.TrackersRepository
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+import kotlinx.coroutines.flow.first
+
+class TrackersAndAppsListsUseCase(
+    private val statsDatabase: StatsDatabase,
+    private val trackersRepository: TrackersRepository,
+    private val appListsRepository: AppListsRepository,
+) {
+
+    suspend fun getAppsAndTrackersCounts(): Pair<List<AppWithTrackersCount>, List<TrackerWithAppsCount>> {
+        val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp()
+        val trackersAndApps = mapIdsToEntities(trackersAndAppsIds)
+        val (countByApp, countByTracker) = foldToCountByEntityMaps(trackersAndApps)
+
+        val appList = buildAppList(countByApp)
+        val trackerList = buildTrackerList(countByTracker)
+        return appList to trackerList
+    }
+
+    private fun buildTrackerList(countByTracker: Map<Tracker, Int>): List<TrackerWithAppsCount> {
+        return countByTracker.map { (tracker, count) ->
+            TrackerWithAppsCount(tracker = tracker, appsCount = count)
+        }.sortedByDescending { it.appsCount }
+    }
+
+    private suspend fun buildAppList(countByApp: Map<ApplicationDescription, Int>): List<AppWithTrackersCount> {
+        return appListsRepository.apps().first().map { app: ApplicationDescription ->
+            AppWithTrackersCount(app = app, trackersCount = countByApp[app] ?: 0)
+        }.sortedByDescending { it.trackersCount }
+    }
+
+    private suspend fun mapIdsToEntities(trackersAndAppsIds: List<Pair<String, String>>): List<Pair<Tracker, ApplicationDescription>> {
+        return trackersAndAppsIds.mapNotNull { (trackerId, apId) ->
+            trackersRepository.getTracker(trackerId)?.let { tracker ->
+                appListsRepository.getDisplayableApp(apId)?.let { app ->
+                    tracker to app
+                }
+            }
+            // appListsRepository.getDisplayableApp() may transform many apId to one
+            // ApplicationDescription, so the lists is not distinct anymore.
+        }.distinct()
+    }
+
+    private fun foldToCountByEntityMaps(trackersAndApps: List<Pair<Tracker, ApplicationDescription>>):
+        Pair<Map<ApplicationDescription, Int>, Map<Tracker, Int>> {
+            return trackersAndApps.fold(
+                mutableMapOf<ApplicationDescription, Int>() to mutableMapOf<Tracker, Int>()
+            ) { (countByApp, countByTracker), (tracker, app) ->
+                countByApp[app] = countByApp.getOrDefault(app, 0) + 1
+                countByTracker[tracker] = countByTracker.getOrDefault(tracker, 0) + 1
+                countByApp to countByTracker
+            }
+        }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt
index 2c47d70..dddc6a2 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt
@@ -41,7 +41,7 @@ class TrackersStateUseCase(
         }
     }
 
-    private fun updateAllTrackersBlockedState() {
+    fun updateAllTrackersBlockedState() {
         localStateRepository.areAllTrackersBlocked.value = whitelistRepository.isBlockingEnabled &&
             whitelistRepository.areWhiteListEmpty()
     }
@@ -50,28 +50,16 @@ class TrackersStateUseCase(
         return isWhitelisted(app, appListsRepository, whitelistRepository)
     }
 
-    fun toggleAppWhitelist(app: ApplicationDescription, isWhitelisted: Boolean) {
-        appListsRepository.applyForHiddenApps(app) {
-            whitelistRepository.setWhiteListed(it.apId, isWhitelisted)
-        }
-        updateAllTrackersBlockedState()
+    fun isWhitelisted(tracker: Tracker): Boolean {
+        return whitelistRepository.isWhiteListed(tracker)
     }
 
-    fun blockTracker(app: ApplicationDescription, tracker: Tracker, isBlocked: Boolean) {
+    suspend fun blockTracker(app: ApplicationDescription, tracker: Tracker, isBlocked: Boolean) {
         appListsRepository.applyForHiddenApps(app) {
             whitelistRepository.setWhiteListed(tracker, it.apId, !isBlocked)
         }
         updateAllTrackersBlockedState()
     }
-
-    fun clearWhitelist(app: ApplicationDescription) {
-        appListsRepository.applyForHiddenApps(
-            app
-        ) {
-            whitelistRepository.clearWhiteList(it.apId)
-        }
-        updateAllTrackersBlockedState()
-    }
 }
 
 fun isWhitelisted(
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt
index 3d6ade0..8f290b8 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt
@@ -1,5 +1,6 @@
 /*
- * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 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
@@ -21,7 +22,6 @@ import android.content.res.Resources
 import foundation.e.advancedprivacy.R
 import foundation.e.advancedprivacy.common.throttleFirst
 import foundation.e.advancedprivacy.data.repositories.AppListsRepository
-import foundation.e.advancedprivacy.domain.entities.AppWithCounts
 import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
 import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
 import foundation.e.advancedprivacy.trackers.data.StatsDatabase
@@ -167,27 +167,7 @@ class TrackersStatisticsUseCase(
         )
     }
 
-    fun getTrackersWithWhiteList(app: ApplicationDescription): List<Pair<Tracker, Boolean>> {
-        return appListsRepository.mapReduceForHiddenApps(
-            app = app,
-            map = { appDesc: ApplicationDescription ->
-                (
-                    statisticsUseCase.getTrackers(listOf(appDesc)) to
-                        getWhiteList(appDesc)
-                    )
-            },
-            reduce = { lists ->
-                lists.unzip().let { (trackerLists, whiteListedIdLists) ->
-                    val whiteListedIds = whiteListedIdLists.flatten().map { it.id }.toSet()
-
-                    trackerLists.flatten().distinctBy { it.id }.sortedBy { it.label.lowercase() }
-                        .map { tracker -> tracker to (tracker.id in whiteListedIds) }
-                }
-            }
-        )
-    }
-
-    fun isWhiteListEmpty(app: ApplicationDescription): Boolean {
+    suspend fun isWhiteListEmpty(app: ApplicationDescription): Boolean {
         return appListsRepository.mapReduceForHiddenApps(
             app = app,
             map = { appDesc: ApplicationDescription ->
@@ -197,7 +177,7 @@ class TrackersStatisticsUseCase(
         )
     }
 
-    fun getCalls(app: ApplicationDescription): Pair<Int, Int> {
+    suspend fun getCalls(app: ApplicationDescription): Pair<Int, Int> {
         return appListsRepository.mapReduceForHiddenApps(
             app = app,
             map = {
@@ -211,67 +191,9 @@ class TrackersStatisticsUseCase(
         )
     }
 
-    fun getAppsWithCounts(): Flow<List<AppWithCounts>> {
-        val trackersCounts = statisticsUseCase.getContactedTrackersCountByApp()
-        val hiddenAppsTrackersWithWhiteList =
-            getTrackersWithWhiteList(appListsRepository.dummySystemApp)
-        val acAppsTrackersWithWhiteList =
-            getTrackersWithWhiteList(appListsRepository.dummyCompatibilityApp)
-
-        return appListsRepository.apps()
-            .map { apps ->
-                val callsByApp = statisticsUseCase.getCallsByApps(24, ChronoUnit.HOURS)
-                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 = !whitelistRepository.isBlockingEnabled ||
-                            isWhitelisted(app, appListsRepository, whitelistRepository),
-                        trackersCount = when (app) {
-                            appListsRepository.dummySystemApp ->
-                                hiddenAppsTrackersWithWhiteList.size
-                            appListsRepository.dummyCompatibilityApp ->
-                                acAppsTrackersWithWhiteList.size
-                            else -> trackersCounts.getOrDefault(app, 0)
-                        },
-                        whiteListedTrackersCount = when (app) {
-                            appListsRepository.dummySystemApp ->
-                                hiddenAppsTrackersWithWhiteList.count { it.second }
-                            appListsRepository.dummyCompatibilityApp ->
-                                acAppsTrackersWithWhiteList.count { it.second }
-                            else ->
-                                getWhiteList(app).size
-                        },
-                        blockedLeaks = calls.first,
-                        leaks = calls.second
-                    )
-                }
-                    .sortedWith(mostLeakedAppsComparator)
-            }
-    }
-
     private fun getWhiteList(app: ApplicationDescription): List<Tracker> {
         return whitelistRepository.getWhiteListForApp(app).mapNotNull {
             trackersRepository.getTracker(it)
         }
     }
-
-    private val mostLeakedAppsComparator: Comparator<AppWithCounts> = Comparator { o1, o2 ->
-        val leaks = o2.leaks - o1.leaks
-        if (leaks != 0) leaks else {
-            val whitelisted = o2.whiteListedTrackersCount - o1.whiteListedTrackersCount
-            if (whitelisted != 0) whitelisted else {
-                o2.trackersCount - o1.trackersCount
-            }
-        }
-    }
 }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
new file mode 100644
index 0000000..f00dff8
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
@@ -0,0 +1,62 @@
+/*
+ * 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.features.trackers
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding
+
+class AppsAdapter(
+    private val viewModel: TrackersViewModel
+) :
+    RecyclerView.Adapter<AppsAdapter.ViewHolder>() {
+
+    class ViewHolder(view: View, private val parentViewModel: TrackersViewModel) : RecyclerView.ViewHolder(view) {
+        val binding = TrackersItemAppBinding.bind(view)
+        fun bind(item: AppWithTrackersCount) {
+            binding.icon.setImageDrawable(item.app.icon)
+            binding.title.text = item.app.label
+            binding.counts.text = itemView.context.getString(R.string.trackers_list_app_trackers_counts, item.trackersCount.toString())
+            itemView.setOnClickListener {
+                parentViewModel.onClickApp(item.app)
+            }
+        }
+    }
+
+    var dataSet: List<AppWithTrackersCount> = emptyList()
+        set(value) {
+            field = value
+            notifyDataSetChanged()
+        }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(parent.context)
+            .inflate(R.layout.trackers_item_app, parent, false)
+        return ViewHolder(view, viewModel)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val app = dataSet[position]
+        holder.bind(app)
+    }
+
+    override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt
new file mode 100644
index 0000000..2420410
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package foundation.e.advancedprivacy.features.trackers
+
+import android.content.Context
+import android.content.res.Resources
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.TrackersListBinding
+
+const val TAB_APPS = 0
+private const val TAB_TRACKERS = 1
+
+class ListsTabPagerAdapter(
+    private val context: Context,
+    private val viewModel: TrackersViewModel,
+) : RecyclerView.Adapter<ListsTabPagerAdapter.ListsTabViewHolder>() {
+    private var apps: List<AppWithTrackersCount> = emptyList()
+    private var trackers: List<TrackerWithAppsCount> = emptyList()
+
+    fun updateDataSet(apps: List<AppWithTrackersCount>?, trackers: List<TrackerWithAppsCount>?) {
+        this.apps = apps ?: emptyList()
+        this.trackers = trackers ?: emptyList()
+        notifyDataSetChanged()
+    }
+
+    override fun getItemViewType(position: Int): Int = position
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListsTabViewHolder {
+        val binding = TrackersListBinding.inflate(LayoutInflater.from(context), parent, false)
+        return when (viewType) {
+            TAB_APPS -> {
+                ListsTabViewHolder.AppsListViewHolder(binding, viewModel)
+            }
+            else -> {
+                ListsTabViewHolder.TrackersListViewHolder(binding, viewModel)
+            }
+        }
+    }
+
+    override fun getItemCount(): Int {
+        return 2
+    }
+
+    override fun onBindViewHolder(holder: ListsTabViewHolder, position: Int) {
+        when (position) {
+            TAB_APPS -> {
+                (holder as ListsTabViewHolder.AppsListViewHolder).onBind(apps)
+            }
+            TAB_TRACKERS -> {
+                (holder as ListsTabViewHolder.TrackersListViewHolder).onBind(trackers)
+            }
+        }
+    }
+
+    sealed class ListsTabViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+        protected fun setupRecyclerView(recyclerView: RecyclerView) {
+            recyclerView.apply {
+                layoutManager = LinearLayoutManager(context)
+                setHasFixedSize(true)
+                addItemDecoration(
+                    MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
+                        dividerColor = ContextCompat.getColor(context, R.color.divider)
+                        dividerInsetStart = 16.dpToPx()
+                        dividerInsetEnd = 16.dpToPx()
+                    }
+                )
+            }
+        }
+
+        private fun Int.dpToPx(): Int {
+            return (this * Resources.getSystem().displayMetrics.density).toInt()
+        }
+
+        class AppsListViewHolder(
+            private val binding: TrackersListBinding,
+            private val viewModel: TrackersViewModel
+        ) : ListsTabViewHolder(binding.root) {
+            init {
+                setupRecyclerView(binding.list)
+                binding.list.adapter = AppsAdapter(viewModel)
+            }
+
+            fun onBind(apps: List<AppWithTrackersCount>) {
+                (binding.list.adapter as AppsAdapter).dataSet = apps
+            }
+        }
+
+        class TrackersListViewHolder(
+            private val binding: TrackersListBinding,
+            private val viewModel: TrackersViewModel
+        ) : ListsTabViewHolder(binding.root) {
+            init {
+                setupRecyclerView(binding.list)
+                binding.list.adapter = TrackersAdapter(viewModel)
+            }
+
+            fun onBind(trackers: List<TrackerWithAppsCount>) {
+                (binding.list.adapter as TrackersAdapter).dataSet = trackers
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt
new file mode 100644
index 0000000..183a5ca
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package foundation.e.advancedprivacy.features.trackers
+
+import android.content.Context
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.ClickableSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.UnderlineSpan
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import foundation.e.advancedprivacy.R
+
+const val URL_LEARN_MORE_ABOUT_TRACKERS = "https://doc.e.foundation/support-topics/advanced_privacy#trackers-blocker"
+
+fun setupDisclaimerBlock(view: TextView, onClickLearnMore: () -> Unit) {
+    with(view) {
+        linksClickable = true
+        isClickable = true
+        movementMethod = android.text.method.LinkMovementMethod.getInstance()
+        text = buildSpan(view.context, onClickLearnMore)
+    }
+}
+
+private fun buildSpan(context: Context, onClickLearnMore: () -> Unit): SpannableString {
+    val start = context.getString(R.string.trackercontroldisclaimer_start)
+    val body = context.getString(R.string.trackercontroldisclaimer_body)
+    val link = context.getString(R.string.trackercontroldisclaimer_link)
+
+    val spannable = SpannableString("$start $body $link")
+
+    val startEndIndex = start.length + 1
+    val linkStartIndex = startEndIndex + body.length + 1
+    val linkEndIndex = spannable.length
+    spannable.setSpan(
+        ForegroundColorSpan(ContextCompat.getColor(context, R.color.primary_text)),
+        0,
+        startEndIndex,
+        Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+    )
+
+    spannable.setSpan(
+        ForegroundColorSpan(ContextCompat.getColor(context, R.color.disabled)),
+        startEndIndex,
+        linkStartIndex,
+        Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+    )
+
+    spannable.setSpan(
+        ForegroundColorSpan(ContextCompat.getColor(context, R.color.accent)),
+        linkStartIndex,
+        linkEndIndex,
+        Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+    )
+    spannable.setSpan(UnderlineSpan(), linkStartIndex, linkEndIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+    spannable.setSpan(
+        object : ClickableSpan() {
+            override fun onClick(p0: View) {
+                onClickLearnMore.invoke()
+            }
+        },
+        linkStartIndex, linkEndIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
+    )
+    return spannable
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt
new file mode 100644
index 0000000..3270bf3
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.features.trackers
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding
+
+class TrackersAdapter(
+    val viewModel: TrackersViewModel
+) :
+    RecyclerView.Adapter<TrackersAdapter.ViewHolder>() {
+
+    class ViewHolder(view: View, private val parentViewModel: TrackersViewModel) : RecyclerView.ViewHolder(view) {
+        val binding = TrackersItemAppBinding.bind(view)
+        init {
+            binding.icon.isVisible = false
+        }
+        fun bind(item: TrackerWithAppsCount) {
+            binding.title.text = item.tracker.label
+            binding.counts.text = itemView.context.getString(R.string.trackers_list_tracker_apps_counts, item.appsCount.toString())
+            itemView.setOnClickListener {
+                parentViewModel.onClickTracker(item.tracker)
+            }
+        }
+    }
+
+    var dataSet: List<TrackerWithAppsCount> = emptyList()
+        set(value) {
+            field = value
+            notifyDataSetChanged()
+        }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(parent.context).inflate(R.layout.trackers_item_app, parent, false)
+        return ViewHolder(view, viewModel)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val app = dataSet[position]
+        holder.bind(app)
+    }
+
+    override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
index 132fa3b..b016c5e 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt
@@ -28,6 +28,7 @@ import android.text.style.ClickableSpan
 import android.text.style.ForegroundColorSpan
 import android.text.style.UnderlineSpan
 import android.view.View
+import android.view.ViewTreeObserver
 import android.widget.Toast
 import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
@@ -35,12 +36,13 @@ import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.viewpager2.widget.ViewPager2
+import com.google.android.material.tabs.TabLayoutMediator
 import foundation.e.advancedprivacy.R
-import foundation.e.advancedprivacy.common.AppsAdapter
 import foundation.e.advancedprivacy.common.GraphHolder
 import foundation.e.advancedprivacy.common.NavToolbarFragment
-import foundation.e.advancedprivacy.common.setToolTipForAsterisk
+import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPosition
+import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild
 import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding
 import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding
 import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
@@ -50,32 +52,98 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
 class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
     private val viewModel: TrackersViewModel by viewModel()
 
-    private var _binding: FragmentTrackersBinding? = null
-    private val binding get() = _binding!!
+    private lateinit var binding: FragmentTrackersBinding
 
     private var dayGraphHolder: GraphHolder? = null
     private var monthGraphHolder: GraphHolder? = null
     private var yearGraphHolder: GraphHolder? = null
 
+    private lateinit var tabAdapter: ListsTabPagerAdapter
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        _binding = FragmentTrackersBinding.bind(view)
+        binding = FragmentTrackersBinding.bind(view)
 
         dayGraphHolder = GraphHolder(binding.graphDay.graph, requireContext(), false)
         monthGraphHolder = GraphHolder(binding.graphMonth.graph, requireContext(), false)
         yearGraphHolder = GraphHolder(binding.graphYear.graph, requireContext(), false)
 
-        binding.apps.apply {
-            layoutManager = LinearLayoutManager(requireContext())
-            setHasFixedSize(true)
-            adapter = AppsAdapter(R.layout.trackers_item_app) { appUid ->
-                viewModel.submitAction(
-                    TrackersViewModel.Action.ClickAppAction(appUid)
-                )
+        tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel)
+        binding.listsPager.adapter = tabAdapter
+
+        TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position ->
+            tab.text = getString(
+                when (position) {
+                    TAB_APPS -> R.string.trackers_toggle_list_apps
+                    else -> R.string.trackers_toggle_list_trackers
+                }
+            )
+        }.attach()
+
+        binding.listsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+            override fun onPageScrollStateChanged(state: Int) {
+                super.onPageScrollStateChanged(state)
+                if (state == ViewPager2.SCROLL_STATE_IDLE) {
+                    updatePagerHeight()
+                }
+            }
+        })
+
+        setupTrackersInfos()
+
+        listenViewModel()
+    }
+
+    private fun listenViewModel() {
+        with(viewLifecycleOwner) {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    render(viewModel.state.value)
+                    viewModel.state.collect(::render)
+                }
+            }
+
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.singleEvents.collect(::handleEvents)
+                }
+            }
+
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.navigate.collect(findNavController()::navigate)
+                }
+            }
+
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.doOnStartedState()
+                }
+            }
+        }
+    }
+
+    private fun handleEvents(event: TrackersViewModel.SingleEvent) {
+        when (event) {
+            is TrackersViewModel.SingleEvent.ErrorEvent -> {
+                displayToast(event.error)
+            }
+            is TrackersViewModel.SingleEvent.OpenUrl -> {
+                try {
+                    startActivity(Intent(Intent.ACTION_VIEW, event.url))
+                } catch (e: ActivityNotFoundException) {
+                    Toast.makeText(
+                        requireContext(),
+                        R.string.error_no_activity_view_url,
+                        Toast.LENGTH_SHORT
+                    ).show()
+                }
             }
         }
+    }
 
+    private fun setupTrackersInfos() {
         val infoText = getString(R.string.trackers_info)
         val moreText = getString(R.string.trackers_info_more)
 
@@ -92,7 +160,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
         spannable.setSpan(
             object : ClickableSpan() {
                 override fun onClick(p0: View) {
-                    viewModel.submitAction(TrackersViewModel.Action.ClickLearnMore)
+                    viewModel.onClickLearnMore()
                 }
             },
             startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
@@ -104,71 +172,44 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
             movementMethod = LinkMovementMethod.getInstance()
             text = spannable
         }
+    }
 
-        setToolTipForAsterisk(
-            textView = binding.trackersAppsListTitle,
-            textId = R.string.trackers_applist_title,
-            tooltipTextId = R.string.trackers_applist_infos
-        )
-
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                render(viewModel.state.value)
-                viewModel.state.collect(::render)
+    private var oldPosition = -1
+    private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
+        binding.listsPager.findViewHolderForAdapterPosition(binding.listsPager.currentItem)
+            .let { currentViewHolder ->
+                currentViewHolder?.itemView?.let { binding.listsPager.updatePagerHeightForChild(it) }
             }
-        }
+    }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.singleEvents.collect { event ->
-                    when (event) {
-                        is TrackersViewModel.SingleEvent.ErrorEvent -> {
-                            displayToast(event.error)
-                        }
-                        is TrackersViewModel.SingleEvent.OpenUrl -> {
-                            try {
-                                startActivity(Intent(Intent.ACTION_VIEW, event.url))
-                            } catch (e: ActivityNotFoundException) {
-                                Toast.makeText(
-                                    requireContext(),
-                                    R.string.error_no_activity_view_url,
-                                    Toast.LENGTH_SHORT
-                                ).show()
-                            }
-                        }
-                    }
-                }
+    private fun updatePagerHeight() {
+        with(binding.listsPager) {
+            val position = currentItem
+            if (position == oldPosition) return
+            if (oldPosition > 0) {
+                val oldItem = findViewHolderForAdapterPosition(oldPosition)?.itemView
+                oldItem?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
             }
-        }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.navigate.collect(findNavController()::navigate)
-            }
-        }
+            val newItem = findViewHolderForAdapterPosition(position)?.itemView
+            newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.doOnStartedState()
-            }
+            oldPosition = position
+            adapter?.notifyItemChanged(position)
         }
     }
 
     private fun displayToast(message: String) {
-        Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
-            .show()
+        Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
     }
 
     private fun render(state: TrackersState) {
         state.dayStatistics?.let { renderGraph(it, dayGraphHolder!!, binding.graphDay) }
         state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) }
         state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) }
+        updatePagerHeight()
 
-        state.apps?.let {
-            binding.apps.post {
-                (binding.apps.adapter as AppsAdapter?)?.dataSet = it
-            }
-        }
+        tabAdapter.updateDataSet(state.apps, state.trackers)
     }
 
     private fun renderGraph(
@@ -191,9 +232,14 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) {
 
     override fun onDestroyView() {
         super.onDestroyView()
+        kotlin.runCatching {
+            if (oldPosition >= 0) {
+                val oldItem = binding.listsPager.findViewHolderForAdapterPosition(oldPosition)
+                oldItem?.itemView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
+            }
+        }
         dayGraphHolder = null
         monthGraphHolder = null
         yearGraphHolder = null
-        _binding = null
     }
 }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
index 13719e4..7f5fdfe 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.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,12 +18,24 @@
 
 package foundation.e.advancedprivacy.features.trackers
 
-import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
 import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
 
 data class TrackersState(
     val dayStatistics: TrackersPeriodicStatistics? = null,
     val monthStatistics: TrackersPeriodicStatistics? = null,
     val yearStatistics: TrackersPeriodicStatistics? = null,
-    val apps: List<AppWithCounts>? = null,
+    val apps: List<AppWithTrackersCount>? = null,
+    val trackers: List<TrackerWithAppsCount>? = null
+)
+
+data class AppWithTrackersCount(
+    val app: ApplicationDescription,
+    val trackersCount: Int = 0
+)
+
+data class TrackerWithAppsCount(
+    val tracker: Tracker,
+    val appsCount: Int = 0
 )
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
index 8a5d0f0..31da8ca 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt
@@ -22,27 +22,24 @@ import android.net.Uri
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.navigation.NavDirections
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase
 import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 class TrackersViewModel(
-    private val trackersStatisticsUseCase: TrackersStatisticsUseCase
+    private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+    private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase
 ) : ViewModel() {
 
-    companion object {
-        private const val URL_LEARN_MORE_ABOUT_TRACKERS =
-            "https://doc.e.foundation/support-topics/advanced_privacy#trackers-blocker"
-    }
-
     private val _state = MutableStateFlow(TrackersState())
     val state = _state.asStateFlow()
 
@@ -53,46 +50,40 @@ class TrackersViewModel(
     val navigate = _navigate.asSharedFlow()
 
     suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
-        merge(
-            trackersStatisticsUseCase.listenUpdates().map {
-                trackersStatisticsUseCase.getDayMonthYearStatistics()
-                    .let { (day, month, year) ->
-                        _state.update { s ->
-                            s.copy(
-                                dayStatistics = day,
-                                monthStatistics = month,
-                                yearStatistics = year
-                            )
-                        }
+        trackersStatisticsUseCase.listenUpdates().collect {
+            trackersStatisticsUseCase.getDayMonthYearStatistics()
+                .let { (day, month, year) ->
+                    _state.update { s ->
+                        s.copy(
+                            dayStatistics = day,
+                            monthStatistics = month,
+                            yearStatistics = year
+                        )
                     }
-            },
-            trackersStatisticsUseCase.getAppsWithCounts().map {
-                _state.update { s -> s.copy(apps = it) }
+                }
+
+            trackersAndAppsListsUseCase.getAppsAndTrackersCounts().let { (appList, trackerList) ->
+                _state.update {
+                    it.copy(apps = appList, trackers = trackerList)
+                }
             }
-        ).collect {}
+        }
     }
 
-    fun submitAction(action: Action) = viewModelScope.launch {
-        when (action) {
-            is Action.ClickAppAction -> actionClickApp(action)
-            is Action.ClickLearnMore ->
-                _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
-        }
+    fun onClickTracker(tracker: Tracker) = viewModelScope.launch {
+        _navigate.emit(TrackersFragmentDirections.gotoTrackerDetailsFragment(trackerId = tracker.id))
     }
 
-    private suspend fun actionClickApp(action: Action.ClickAppAction) {
-        state.value.apps?.find { it.uid == action.appUid }?.let {
-            _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = it.uid))
-        }
+    fun onClickApp(app: ApplicationDescription) = viewModelScope.launch {
+        _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = app.uid))
+    }
+
+    fun onClickLearnMore() = viewModelScope.launch {
+        _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
     }
 
     sealed class SingleEvent {
         data class ErrorEvent(val error: String) : SingleEvent()
         data class OpenUrl(val url: Uri) : SingleEvent()
     }
-
-    sealed class Action {
-        data class ClickAppAction(val appUid: Int) : Action()
-        object ClickLearnMore : Action()
-    }
 }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
index 7fb9ca6..85c5350 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt
@@ -23,16 +23,19 @@ import android.content.Intent
 import android.os.Bundle
 import android.view.View
 import android.widget.Toast
+import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.navigation.fragment.navArgs
 import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
 import com.google.android.material.snackbar.Snackbar
 import foundation.e.advancedprivacy.R
 import foundation.e.advancedprivacy.common.NavToolbarFragment
 import foundation.e.advancedprivacy.databinding.ApptrackersFragmentBinding
+import foundation.e.advancedprivacy.features.trackers.setupDisclaimerBlock
 import kotlinx.coroutines.launch
 import org.koin.androidx.viewmodel.ext.android.viewModel
 import org.koin.core.parameter.parametersOf
@@ -42,8 +45,7 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
     private val args: AppTrackersFragmentArgs by navArgs()
     private val viewModel: AppTrackersViewModel by viewModel { parametersOf(args.appUid) }
 
-    private var _binding: ApptrackersFragmentBinding? = null
-    private val binding get() = _binding!!
+    private lateinit var binding: ApptrackersFragmentBinding
 
     override fun getTitle(): CharSequence {
         return ""
@@ -56,96 +58,111 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-        _binding = ApptrackersFragmentBinding.bind(view)
+        binding = ApptrackersFragmentBinding.bind(view)
 
         binding.blockAllToggle.setOnClickListener {
-            viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked))
-        }
-        binding.btnReset.setOnClickListener {
-            viewModel.submitAction(AppTrackersViewModel.Action.ResetAllTrackers)
+            viewModel.onToggleBlockAll(binding.blockAllToggle.isChecked)
         }
+        binding.btnReset.setOnClickListener { viewModel.onClickResetAllTrackers() }
 
-        binding.trackers.apply {
+        binding.list.apply {
             layoutManager = LinearLayoutManager(requireContext())
             setHasFixedSize(true)
-            adapter = ToggleTrackersAdapter(
-                R.layout.apptrackers_item_tracker_toggle,
-                onToggleSwitch = { tracker, isBlocked ->
-                    viewModel.submitAction(AppTrackersViewModel.Action.ToggleTrackerAction(tracker, isBlocked))
-                },
-                onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) },
+            addItemDecoration(
+                MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL).apply {
+                    dividerColor = ContextCompat.getColor(requireContext(), R.color.divider)
+                }
             )
+            adapter = ToggleTrackersAdapter(viewModel)
         }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.singleEvents.collect { event ->
-                    when (event) {
-                        is AppTrackersViewModel.SingleEvent.ErrorEvent ->
-                            displayToast(getString(event.errorResId))
-                        is AppTrackersViewModel.SingleEvent.OpenUrl ->
-                            try {
-                                startActivity(Intent(Intent.ACTION_VIEW, event.url))
-                            } catch (e: ActivityNotFoundException) {
-                                Toast.makeText(
-                                    requireContext(),
-                                    R.string.error_no_activity_view_url,
-                                    Toast.LENGTH_SHORT
-                                ).show()
-                            }
-                        is AppTrackersViewModel.SingleEvent.ToastTrackersControlDisabled ->
-                            Snackbar.make(
-                                binding.root,
-                                R.string.apptrackers_tracker_control_disabled_message,
-                                Snackbar.LENGTH_LONG
-                            ).show()
-                    }
+        listenViewModel()
+
+        setupDisclaimerBlock(binding.disclaimerBlockTrackers.root, viewModel::onClickLearnMore)
+    }
+
+    private fun listenViewModel() {
+        with(viewLifecycleOwner) {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.singleEvents.collect(::handleEvents)
                 }
             }
-        }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.doOnStartedState()
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.doOnStartedState()
+                }
             }
-        }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                render(viewModel.state.value)
-                viewModel.state.collect(::render)
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    render(viewModel.state.value)
+                    viewModel.state.collect(::render)
+                }
             }
         }
     }
+    private fun handleEvents(event: AppTrackersViewModel.SingleEvent) {
+        when (event) {
+            is AppTrackersViewModel.SingleEvent.ErrorEvent ->
+                displayToast(getString(event.errorResId))
+
+            is AppTrackersViewModel.SingleEvent.OpenUrl ->
+                try {
+                    startActivity(Intent(Intent.ACTION_VIEW, event.url))
+                } catch (e: ActivityNotFoundException) {
+                    Toast.makeText(
+                        requireContext(),
+                        R.string.error_no_activity_view_url,
+                        Toast.LENGTH_SHORT
+                    ).show()
+                }
+
+            is AppTrackersViewModel.SingleEvent.ToastTrackersControlDisabled ->
+                Snackbar.make(
+                    binding.root,
+                    R.string.apptrackers_tracker_control_disabled_message,
+                    Snackbar.LENGTH_LONG
+                ).show()
+        }
+    }
 
     private fun render(state: AppTrackersState) {
         setTitle(state.appDesc?.label)
-        binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) ""
-        else getString(
-            R.string.apptrackers_trackers_count_summary,
-            state.getBlockedTrackersCount(),
-            state.getTrackersCount(),
-            state.blocked,
-            state.leaked
-        )
+        binding.subtitle.text = getString(R.string.apptrackers_subtitle, state.appDesc?.label)
+        binding.dataDetectedTrackers.apply {
+            primaryMessage.setText(R.string.apptrackers_detected_tracker_primary)
+            number.text = state.getTrackersCount().toString()
+            secondaryMessage.setText(R.string.apptrackers_detected_tracker_secondary)
+        }
+
+        binding.dataBlockedTrackers.apply {
+            primaryMessage.setText(R.string.apptrackers_blocked_tracker_primary)
+            number.text = state.getBlockedTrackersCount().toString()
+            secondaryMessage.setText(R.string.apptrackers_blocked_tracker_secondary)
+        }
+
+        binding.dataBlockedLeaks.apply {
+            primaryMessage.setText(R.string.apptrackers_blocked_leaks_primary)
+            number.text = state.blocked.toString()
+            secondaryMessage.text = getString(R.string.apptrackers_blocked_leaks_secondary, state.leaked.toString())
+        }
 
         binding.blockAllToggle.isChecked = state.isBlockingActivated
 
-        val trackersStatus = state.getTrackersStatus()
-        if (!trackersStatus.isNullOrEmpty()) {
-            binding.trackersListTitle.isVisible = state.isBlockingActivated
-            binding.trackers.isVisible = true
-            binding.trackers.post {
-                (binding.trackers.adapter as ToggleTrackersAdapter?)?.updateDataSet(
-                    trackersStatus,
-                    state.isBlockingActivated
-                )
+        val trackersStatus = state.trackersWithBlockedList
+        if (!trackersStatus.isEmpty()) {
+            binding.listTitle.isVisible = true
+            binding.list.isVisible = true
+            binding.list.post {
+                (binding.list.adapter as ToggleTrackersAdapter?)?.updateDataSet(trackersStatus)
             }
             binding.noTrackersYet.isVisible = false
             binding.btnReset.isVisible = true
         } else {
-            binding.trackersListTitle.isVisible = false
-            binding.trackers.isVisible = false
+            binding.listTitle.isVisible = false
+            binding.list.isVisible = false
             binding.noTrackersYet.isVisible = true
             binding.noTrackersYet.text = getString(
                 when {
@@ -157,9 +174,4 @@ class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) {
             binding.btnReset.isVisible = state.isBlockingActivated && !state.isWhitelistEmpty
         }
     }
-
-    override fun onDestroyView() {
-        super.onDestroyView()
-        _binding = null
-    }
 }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
index a597da6..cea99a6 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt
@@ -24,19 +24,13 @@ import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
 data class AppTrackersState(
     val appDesc: ApplicationDescription? = null,
     val isBlockingActivated: Boolean = false,
-    val trackersWithWhiteList: List<Pair<Tracker, Boolean>>? = null,
+    val trackersWithBlockedList: List<Pair<Tracker, Boolean>> = emptyList(),
     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>>? {
-        return trackersWithWhiteList?.map { it.first to !it.second }
-    }
+    fun getTrackersCount() = trackersWithBlockedList.size
 
-    fun getTrackersCount() = trackersWithWhiteList?.size ?: 0
-    fun getBlockedTrackersCount(): Int = if (isTrackersBlockingEnabled && isBlockingActivated)
-        trackersWithWhiteList?.count { !it.second } ?: 0
-    else 0
+    fun getBlockedTrackersCount(): Int = trackersWithBlockedList.count { it.second }
 }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
index 8740779..00ad365 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt
@@ -24,9 +24,11 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
 import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.domain.usecases.AppTrackersUseCase
 import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
 import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
 import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.features.trackers.URL_LEARN_MORE_ABOUT_TRACKERS
 import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableSharedFlow
@@ -41,6 +43,7 @@ import kotlinx.coroutines.withContext
 
 class AppTrackersViewModel(
     private val app: ApplicationDescription,
+    private val appTrackersUseCase: AppTrackersUseCase,
     private val trackersStateUseCase: TrackersStateUseCase,
     private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
     private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
@@ -56,17 +59,10 @@ class AppTrackersViewModel(
     val singleEvents = _singleEvents.asSharedFlow()
 
     init {
-        viewModelScope.launch(Dispatchers.IO) {
-            _state.update {
-                it.copy(
-                    appDesc = app,
-                    isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
-                    trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(
-                        app
-                    ),
-                    isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
-                )
-            }
+        _state.update {
+            it.copy(
+                appDesc = app,
+            )
         }
     }
 
@@ -79,80 +75,71 @@ class AppTrackersViewModel(
         ).collect { }
     }
 
-    fun submitAction(action: Action) = viewModelScope.launch {
-        when (action) {
-            is Action.BlockAllToggleAction -> blockAllToggleAction(action)
-            is Action.ToggleTrackerAction -> toggleTrackerAction(action)
-            is Action.ClickTracker -> actionClickTracker(action)
-            is Action.ResetAllTrackers -> resetAllTrackers()
+    fun onClickLearnMore() {
+        viewModelScope.launch {
+            _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
         }
     }
 
-    private suspend fun blockAllToggleAction(action: Action.BlockAllToggleAction) {
-        withContext(Dispatchers.IO) {
+    fun onToggleBlockAll(isBlocked: Boolean) {
+        viewModelScope.launch(Dispatchers.IO) {
             if (!state.value.isTrackersBlockingEnabled) {
                 _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
             }
-            trackersStateUseCase.toggleAppWhitelist(app, !action.isBlocked)
-            _state.update {
-                it.copy(
-                    isBlockingActivated = !trackersStateUseCase.isWhitelisted(app)
-                )
-            }
+            appTrackersUseCase.toggleAppWhitelist(app, isBlocked)
+            updateWhitelist()
         }
     }
 
-    private suspend fun toggleTrackerAction(action: Action.ToggleTrackerAction) {
-        withContext(Dispatchers.IO) {
+    fun onToggleTracker(tracker: Tracker, isBlocked: Boolean) {
+        viewModelScope.launch(Dispatchers.IO) {
             if (!state.value.isTrackersBlockingEnabled) {
                 _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
             }
 
-            if (state.value.isBlockingActivated) {
-                trackersStateUseCase.blockTracker(app, action.tracker, action.isBlocked)
-                updateWhitelist()
-            }
+            trackersStateUseCase.blockTracker(app, tracker, isBlocked)
+            updateWhitelist()
         }
     }
 
-    private suspend fun actionClickTracker(action: Action.ClickTracker) {
-        withContext(Dispatchers.IO) {
-            action.tracker.exodusId?.let {
-                try {
-                    _singleEvents.emit(
-                        SingleEvent.OpenUrl(
-                            Uri.parse(exodusBaseUrl + it)
-                        )
-                    )
-                } catch (e: Exception) {
-                }
-            }
+    fun onClickTracker(tracker: Tracker) {
+        viewModelScope.launch(Dispatchers.IO) {
+            tracker.exodusId?.let {
+                runCatching { Uri.parse(exodusBaseUrl + it) }.getOrNull()
+            }?.let { _singleEvents.emit(SingleEvent.OpenUrl(it)) }
         }
     }
 
-    private suspend fun resetAllTrackers() {
-        withContext(Dispatchers.IO) {
-            trackersStateUseCase.clearWhitelist(app)
+    fun onClickResetAllTrackers() {
+        viewModelScope.launch(Dispatchers.IO) {
+            appTrackersUseCase.clearWhitelist(app)
             updateWhitelist()
         }
     }
-    private fun fetchStatistics() {
-        val (blocked, leaked) = trackersStatisticsUseCase.getCalls(app)
-        return _state.update { s ->
+
+    private suspend fun fetchStatistics() = withContext(Dispatchers.IO) {
+        val (blocked, leaked) = appTrackersUseCase.getCalls(app)
+        val trackersWithBlockedList = appTrackersUseCase.getTrackersWithBlockedList(app)
+
+        _state.update { s ->
             s.copy(
-                trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
                 leaked = leaked,
                 blocked = blocked,
-                isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+                isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
+                isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app),
+                trackersWithBlockedList = trackersWithBlockedList
             )
         }
     }
 
-    private fun updateWhitelist() {
+    private suspend fun updateWhitelist() = withContext(Dispatchers.IO) {
         _state.update { s ->
             s.copy(
-                trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app),
-                isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app)
+                isBlockingActivated = !trackersStateUseCase.isWhitelisted(app),
+                trackersWithBlockedList = appTrackersUseCase.enrichWithBlockedState(
+                    app, s.trackersWithBlockedList.map { it.first }
+                ),
+                isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app),
             )
         }
     }
@@ -162,11 +149,4 @@ class AppTrackersViewModel(
         data class OpenUrl(val url: Uri) : SingleEvent()
         object ToastTrackersControlDisabled : SingleEvent()
     }
-
-    sealed class Action {
-        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/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
index ef845b6..1d49905 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.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
@@ -20,72 +21,67 @@ package foundation.e.advancedprivacy.features.trackers.apptrackers
 import android.text.SpannableString
 import android.text.style.UnderlineSpan
 import android.view.LayoutInflater
-import android.view.View
 import android.view.ViewGroup
-import android.widget.Switch
-import android.widget.TextView
 import androidx.core.content.ContextCompat
 import androidx.recyclerview.widget.RecyclerView
 import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.databinding.ApptrackersItemTrackerToggleBinding
 import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
 
 class ToggleTrackersAdapter(
-    private val itemsLayout: Int,
-    private val onToggleSwitch: (Tracker, Boolean) -> Unit,
-    private val onClickTitle: (Tracker) -> Unit
+    private val viewModel: AppTrackersViewModel
 ) : RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() {
-
-    var isEnabled = true
-
     class ViewHolder(
-        view: View,
-        private val onToggleSwitch: (Tracker, Boolean) -> Unit,
-        private val onClickTitle: (Tracker) -> Unit
-    ) : RecyclerView.ViewHolder(view) {
-        val title: TextView = view.findViewById(R.id.title)
+        private val binding: ApptrackersItemTrackerToggleBinding,
+        private val viewModel: AppTrackersViewModel,
+    ) : RecyclerView.ViewHolder(binding.root) {
 
-        val toggle: Switch = view.findViewById(R.id.toggle)
+        fun bind(item: Pair<Tracker, Boolean>) {
+            val label = item.first.label
+            with(binding.title) {
+                if (item.first.exodusId != null) {
 
-        fun bind(item: Pair<Tracker, Boolean>, isEnabled: Boolean) {
-            val text = item.first.label
-            if (item.first.exodusId != null) {
-                title.setTextColor(ContextCompat.getColor(title.context, R.color.accent))
-                val spannable = SpannableString(text)
-                spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0)
-                title.text = spannable
-            } else {
-                title.setTextColor(ContextCompat.getColor(title.context, R.color.primary_text))
-                title.text = text
+                    setTextColor(ContextCompat.getColor(context, R.color.accent))
+                    val spannable = SpannableString(label)
+                    spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0)
+                    text = spannable
+                } else {
+                    setTextColor(ContextCompat.getColor(context, R.color.primary_text))
+                    text = label
+                }
+                setOnClickListener { viewModel.onClickTracker(item.first) }
             }
+            with(binding.toggle) {
+                isChecked = item.second
 
-            toggle.isChecked = item.second
-            toggle.isEnabled = isEnabled
-
-            toggle.setOnClickListener {
-                onToggleSwitch(item.first, toggle.isChecked)
+                setOnClickListener {
+                    viewModel.onToggleTracker(item.first, isChecked)
+                }
             }
-
-            title.setOnClickListener { onClickTitle(item.first) }
         }
     }
 
     private var dataSet: List<Pair<Tracker, Boolean>> = emptyList()
 
-    fun updateDataSet(new: List<Pair<Tracker, Boolean>>, isEnabled: Boolean) {
-        this.isEnabled = isEnabled
+    fun updateDataSet(new: List<Pair<Tracker, Boolean>>) {
         dataSet = new
         notifyDataSetChanged()
     }
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-        val view = LayoutInflater.from(parent.context)
-            .inflate(itemsLayout, parent, false)
-        return ViewHolder(view, onToggleSwitch, onClickTitle)
+        return ViewHolder(
+            ApptrackersItemTrackerToggleBinding.inflate(
+                LayoutInflater.from(parent.context),
+                parent,
+                false
+            ),
+            viewModel
+        )
     }
 
     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
         val permission = dataSet[position]
-        holder.bind(permission, isEnabled)
+        holder.bind(permission)
     }
 
     override fun getItemCount(): Int = dataSet.size
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt
new file mode 100644
index 0000000..d419677
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.features.trackers.trackerdetails
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.databinding.ApptrackersItemTrackerToggleBinding
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+
+class TrackerAppsAdapter(
+    private val viewModel: TrackerDetailsViewModel
+) : RecyclerView.Adapter<TrackerAppsAdapter.ViewHolder>() {
+
+    class ViewHolder(
+        private val binding: ApptrackersItemTrackerToggleBinding,
+        private val viewModel: TrackerDetailsViewModel,
+    ) : RecyclerView.ViewHolder(binding.root) {
+
+        fun bind(item: Pair<ApplicationDescription, Boolean>) {
+            val (app, isWhiteListed) = item
+            binding.title.text = app.label
+            binding.toggle.apply {
+                this.isChecked = isWhiteListed
+                setOnClickListener {
+                    viewModel.onToggleUnblockApp(app, isChecked)
+                }
+            }
+        }
+    }
+
+    private var dataSet: List<Pair<ApplicationDescription, Boolean>> = emptyList()
+
+    fun updateDataSet(new: List<Pair<ApplicationDescription, Boolean>>) {
+        dataSet = new
+        notifyDataSetChanged()
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        return ViewHolder(
+            ApptrackersItemTrackerToggleBinding.inflate(LayoutInflater.from(parent.context), parent, false),
+            viewModel
+        )
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val permission = dataSet[position]
+        holder.bind(permission)
+    }
+
+    override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt
new file mode 100644
index 0000000..481c809
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.features.trackers.trackerdetails
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.core.content.ContextCompat.getColor
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import com.google.android.material.snackbar.Snackbar
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.NavToolbarFragment
+import foundation.e.advancedprivacy.databinding.TrackerdetailsFragmentBinding
+import foundation.e.advancedprivacy.features.trackers.setupDisclaimerBlock
+import kotlinx.coroutines.launch
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.parameter.parametersOf
+
+class TrackerDetailsFragment : NavToolbarFragment(R.layout.trackerdetails_fragment) {
+
+    private val args: TrackerDetailsFragmentArgs by navArgs()
+    private val viewModel: TrackerDetailsViewModel by viewModel { parametersOf(args.trackerId) }
+
+    private lateinit var binding: TrackerdetailsFragmentBinding
+
+    override fun getTitle(): CharSequence {
+        return ""
+    }
+
+    private fun displayToast(message: String) {
+        Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+            .show()
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        binding = TrackerdetailsFragmentBinding.bind(view)
+
+        binding.blockAllToggle.setOnClickListener {
+            viewModel.onToggleBlockAll(binding.blockAllToggle.isChecked)
+        }
+
+        binding.apps.apply {
+            layoutManager = LinearLayoutManager(requireContext())
+            setHasFixedSize(true)
+            addItemDecoration(
+                MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL).apply {
+                    dividerColor = getColor(requireContext(), R.color.divider)
+                }
+            )
+            adapter = TrackerAppsAdapter(viewModel)
+        }
+
+        setupDisclaimerBlock(binding.disclaimerBlockTrackers.root, viewModel::onClickLearnMore)
+
+        listenViewModel()
+    }
+
+    private fun listenViewModel() {
+        with(viewLifecycleOwner) {
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.singleEvents.collect(::handleEvents)
+                }
+            }
+
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    viewModel.doOnStartedState()
+                }
+            }
+
+            lifecycleScope.launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    render(viewModel.state.value)
+                    viewModel.state.collect(::render)
+                }
+            }
+        }
+    }
+
+    private fun handleEvents(event: TrackerDetailsViewModel.SingleEvent) {
+        when (event) {
+            is TrackerDetailsViewModel.SingleEvent.ErrorEvent ->
+                displayToast(getString(event.errorResId))
+            is TrackerDetailsViewModel.SingleEvent.ToastTrackersControlDisabled ->
+                Snackbar.make(
+                    binding.root,
+                    R.string.apptrackers_tracker_control_disabled_message,
+                    Snackbar.LENGTH_LONG
+                ).show()
+            is TrackerDetailsViewModel.SingleEvent.OpenUrl -> {
+                try {
+                    startActivity(Intent(Intent.ACTION_VIEW, event.url))
+                } catch (e: ActivityNotFoundException) {
+                    Toast.makeText(
+                        requireContext(),
+                        R.string.error_no_activity_view_url,
+                        Toast.LENGTH_SHORT
+                    ).show()
+                }
+            }
+        }
+    }
+
+    private fun render(state: TrackerDetailsState) {
+        setTitle(state.tracker?.label)
+        binding.subtitle.text = getString(R.string.trackerdetails_subtitle, state.tracker?.label)
+        binding.dataAppCount.apply {
+            primaryMessage.setText(R.string.trackerdetails_app_count_primary)
+            number.text = state.detectedCount.toString()
+            secondaryMessage.setText(R.string.trackerdetails_app_count_secondary)
+        }
+
+        binding.dataBlockedLeaks.apply {
+            primaryMessage.setText(R.string.trackerdetails_blocked_leaks_primary)
+            number.text = state.blockedCount.toString()
+            secondaryMessage.text = getString(R.string.trackerdetails_blocked_leaks_secondary, state.leakedCount.toString())
+        }
+
+        binding.blockAllToggle.isChecked = state.isBlockAllActivated
+
+        binding.apps.post {
+            (binding.apps.adapter as TrackerAppsAdapter?)?.updateDataSet(state.appList)
+        }
+    }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt
new file mode 100644
index 0000000..9ae7412
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.features.trackers.trackerdetails
+
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+
+data class TrackerDetailsState(
+    val tracker: Tracker? = null,
+    val isBlockAllActivated: Boolean = false,
+    val detectedCount: Int = 0,
+    val blockedCount: Int = 0,
+    val leakedCount: Int = 0,
+    val appList: List<Pair<ApplicationDescription, Boolean>> = emptyList(),
+    val isTrackersBlockingEnabled: Boolean = false,
+)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt
new file mode 100644
index 0000000..91a1f2a
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 MURENA SAS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.features.trackers.trackerdetails
+
+import android.net.Uri
+import androidx.annotation.StringRes
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
+import foundation.e.advancedprivacy.domain.entities.TrackerMode
+import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackerDetailsUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
+import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
+import foundation.e.advancedprivacy.features.trackers.URL_LEARN_MORE_ABOUT_TRACKERS
+import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class TrackerDetailsViewModel(
+    private val tracker: Tracker,
+    private val trackersStateUseCase: TrackersStateUseCase,
+    private val trackersStatisticsUseCase: TrackersStatisticsUseCase,
+    private val trackerDetailsUseCase: TrackerDetailsUseCase,
+    private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase
+) : ViewModel() {
+    private val _state = MutableStateFlow(TrackerDetailsState(tracker = tracker))
+    val state = _state.asStateFlow()
+
+    private val _singleEvents = MutableSharedFlow<SingleEvent>()
+    val singleEvents = _singleEvents.asSharedFlow()
+
+    suspend fun doOnStartedState() = withContext(Dispatchers.IO) {
+        merge(
+            getQuickPrivacyStateUseCase.trackerMode.map {
+                _state.update { s -> s.copy(isTrackersBlockingEnabled = it != TrackerMode.VULNERABLE) }
+            },
+            trackersStatisticsUseCase.listenUpdates().map { fetchStatistics() }
+        ).collect { }
+    }
+
+    fun onToggleUnblockApp(app: ApplicationDescription, isBlocked: Boolean) {
+        viewModelScope.launch(Dispatchers.IO) {
+            if (!state.value.isTrackersBlockingEnabled) {
+                _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
+            }
+
+            trackersStateUseCase.blockTracker(app, tracker, isBlocked)
+            updateWhitelist()
+        }
+    }
+
+    fun onToggleBlockAll(isBlocked: Boolean) {
+        viewModelScope.launch(Dispatchers.IO) {
+            if (!state.value.isTrackersBlockingEnabled) {
+                _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled)
+            }
+            trackerDetailsUseCase.toggleTrackerWhitelist(tracker, isBlocked)
+            _state.update {
+                it.copy(
+                    isBlockAllActivated = !trackersStateUseCase.isWhitelisted(tracker)
+                )
+            }
+            updateWhitelist()
+        }
+    }
+
+    fun onClickLearnMore() {
+        viewModelScope.launch {
+            _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS)))
+        }
+    }
+
+    private suspend fun fetchStatistics() = withContext(Dispatchers.IO) {
+        val (blocked, leaked) = trackerDetailsUseCase.getCalls(tracker)
+        val appsWhitWhiteListState = trackerDetailsUseCase.getAppsWithBlockedState(tracker)
+
+        _state.update { s ->
+            s.copy(
+                isBlockAllActivated = !trackersStateUseCase.isWhitelisted(tracker),
+                detectedCount = appsWhitWhiteListState.size,
+                blockedCount = blocked,
+                leakedCount = leaked,
+                appList = appsWhitWhiteListState,
+            )
+        }
+    }
+
+    private suspend fun updateWhitelist() {
+        _state.update { s ->
+            s.copy(
+                isBlockAllActivated = !trackersStateUseCase.isWhitelisted(tracker),
+                appList = trackerDetailsUseCase.enrichWithBlockedState(
+                    s.appList.map { it.first }, tracker
+                )
+            )
+        }
+    }
+
+    sealed class SingleEvent {
+        data class ErrorEvent(@StringRes val errorResId: Int) : SingleEvent()
+        object ToastTrackersControlDisabled : SingleEvent()
+        data class OpenUrl(val url: Uri) : SingleEvent()
+    }
+}
-- 
cgit v1.2.1