summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-12-05 08:17:01 +0000
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-12-05 08:17:01 +0000
commit2e897cc8af4234abc4e3f5c3448e1fd7b2b8a1bd (patch)
tree8f72170bee6247db6743521675d0ac0822b2ef65 /app/src/main/java
parent0db4d25038823369f320e0cd291968e66ed51e0c (diff)
1203 trackers oriented view
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt27
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/extensions/ViewPager2Extensions.kt43
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/AppTrackersUseCase.kt83
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackerDetailsUseCase.kt55
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt78
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStateUseCase.kt20
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt86
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt (renamed from app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt)39
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt123
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackerControlDisclaimer.kt81
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt64
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt172
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt17
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt67
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersFragment.kt152
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersState.kt12
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/AppTrackersViewModel.kt102
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/apptrackers/ToggleTrackersAdapter.kt72
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerAppsAdapter.kt67
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsFragment.kt149
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsState.kt31
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/features/trackers/trackerdetails/TrackerDetailsViewModel.kt128
22 files changed, 1264 insertions, 404 deletions
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/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/common/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
index aee1890..f00dff8 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt
@@ -15,42 +15,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-package foundation.e.advancedprivacy.common
+package foundation.e.advancedprivacy.features.trackers
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
+import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding
class AppsAdapter(
- private val itemsLayout: Int,
- private val listener: (Int) -> Unit
+ private val viewModel: TrackersViewModel
) :
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) }
+ 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<AppWithCounts> = emptyList()
+ var dataSet: List<AppWithTrackersCount> = emptyList()
set(value) {
field = value
notifyDataSetChanged()
@@ -58,8 +49,8 @@ class AppsAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
- .inflate(itemsLayout, parent, false)
- return ViewHolder(view, listener)
+ .inflate(R.layout.trackers_item_app, parent, false)
+ return ViewHolder(view, viewModel)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
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()
+ }
+}