summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle2
-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
-rw-r--r--app/src/main/res/drawable/bg_stroke_rounded_12.xml21
-rw-r--r--app/src/main/res/drawable/ic_shield_alert.xml15
-rw-r--r--app/src/main/res/drawable/pill_shape_tab_bg.xml22
-rw-r--r--app/src/main/res/drawable/pill_shape_tab_indicator.xml21
-rw-r--r--app/src/main/res/drawable/pill_shape_tab_selected.xml22
-rw-r--r--app/src/main/res/layout/apptrackers_fragment.xml208
-rw-r--r--app/src/main/res/layout/apptrackers_item_tracker_toggle.xml22
-rw-r--r--app/src/main/res/layout/disclaimer_block_trackers.xml33
-rw-r--r--app/src/main/res/layout/fragment_fake_location.xml19
-rw-r--r--app/src/main/res/layout/fragment_trackers.xml67
-rw-r--r--app/src/main/res/layout/highlight_data_number.xml58
-rw-r--r--app/src/main/res/layout/trackerdetails_fragment.xml135
-rw-r--r--app/src/main/res/layout/trackers_item_app.xml44
-rw-r--r--app/src/main/res/layout/trackers_list.xml24
-rw-r--r--app/src/main/res/navigation/nav_graph.xml11
-rw-r--r--app/src/main/res/values/colors.xml23
-rw-r--r--app/src/main/res/values/strings.xml57
-rw-r--r--core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt33
-rw-r--r--gradle/libs.versions.toml1
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt165
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt225
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt10
-rw-r--r--trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt13
46 files changed, 2291 insertions, 628 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 95bbee6..a10ead1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -163,7 +163,7 @@ dependencies {
libs.androidx.fragment.ktx,
libs.androidx.lifecycle.runtime,
libs.androidx.lifecycle.viewmodel,
-
+ libs.androidx.viewpager2,
libs.bundles.koin,
libs.google.material,
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()
+ }
+}
diff --git a/app/src/main/res/drawable/bg_stroke_rounded_12.xml b/app/src/main/res/drawable/bg_stroke_rounded_12.xml
new file mode 100644
index 0000000..d9c839c
--- /dev/null
+++ b/app/src/main/res/drawable/bg_stroke_rounded_12.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke android:width="1dp" android:color="@color/divider" />
+ <corners android:radius="12dp" />
+</shape> \ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_shield_alert.xml b/app/src/main/res/drawable/ic_shield_alert.xml
new file mode 100644
index 0000000..9c20541
--- /dev/null
+++ b/app/src/main/res/drawable/ic_shield_alert.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,2L4,5V11.09C4,16.14 7.41,20.85 12,22C16.59,20.85 20,16.14 20,11.09V5L12,2ZM18,11.09C18,15.09 15.45,18.79 12,19.92C8.55,18.79 6,15.1 6,11.09V6.39L12,4.14L18,6.39V11.09Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M13,14H11V16H13V14Z"
+ android:fillColor="#000000"/>
+ <path
+ android:pathData="M13,7H11V12H13V7Z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/app/src/main/res/drawable/pill_shape_tab_bg.xml b/app/src/main/res/drawable/pill_shape_tab_bg.xml
new file mode 100644
index 0000000..5ef1de5
--- /dev/null
+++ b/app/src/main/res/drawable/pill_shape_tab_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="200dp"/>
+ <solid android:color="@color/background_overlay" />
+</shape>
diff --git a/app/src/main/res/drawable/pill_shape_tab_indicator.xml b/app/src/main/res/drawable/pill_shape_tab_indicator.xml
new file mode 100644
index 0000000..344a049
--- /dev/null
+++ b/app/src/main/res/drawable/pill_shape_tab_indicator.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/pill_shape_tab_selected" android:state_selected="true"/>
+ <item android:drawable="@android:color/transparent" android:state_selected="false"/>
+</selector> \ No newline at end of file
diff --git a/app/src/main/res/drawable/pill_shape_tab_selected.xml b/app/src/main/res/drawable/pill_shape_tab_selected.xml
new file mode 100644
index 0000000..520d985
--- /dev/null
+++ b/app/src/main/res/drawable/pill_shape_tab_selected.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="200dp"/>
+ <solid android:color="@color/e_accent" />
+</shape> \ No newline at end of file
diff --git a/app/src/main/res/layout/apptrackers_fragment.xml b/app/src/main/res/layout/apptrackers_fragment.xml
index d0a72d5..06b8d3f 100644
--- a/app/src/main/res/layout/apptrackers_fragment.xml
+++ b/app/src/main/res/layout/apptrackers_fragment.xml
@@ -15,18 +15,15 @@
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
-
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
- >
-
- <androidx.coordinatorlayout.widget.CoordinatorLayout
- android:background="@color/background"
- android:layout_height="match_parent"
- android:layout_width="match_parent"
- >
+ android:background="@color/background"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ >
<include layout="@layout/topbar" />
@@ -37,80 +34,137 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
- <LinearLayout
- android:layout_height="match_parent"
- android:layout_width="match_parent"
- android:orientation="vertical"
- >
- <TextView
- android:id="@+id/trackers_count_summary"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="16sp"
- android:padding="16dp"
- />
<LinearLayout
+ android:layout_height="match_parent"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:padding="16dp"
- android:gravity="center_vertical">
+ android:orientation="vertical"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="32dp"
+ >
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
+ android:textColor="@color/disabled"
+ android:layout_marginBottom="16dp"
+ tools:text="Facebook tracking summary"
+ />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginBottom="16dp"
+ android:baselineAligned="false">
+ <include
+ android:id="@+id/data_detected_trackers"
+ layout="@layout/highlight_data_number"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="16dp"
+ android:layout_weight="1"
+ />
+ <include
+ android:id="@+id/data_blocked_trackers"
+ layout="@layout/highlight_data_number"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ />
+ </LinearLayout>
+ <include
+ android:id="@+id/data_blocked_leaks"
+ layout="@layout/highlight_data_number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+
<TextView
- android:layout_width="0dp"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:text="@string/apptrackers_block_all_toggle"
+ android:textSize="14sp"
+ android:lineHeight="24sp"
+ android:textFontWeight="500"
+ android:textColor="@color/primary_text"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/trackerdetails_manage_tracker"
/>
- <Switch
- android:id="@+id/block_all_toggle"
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingVertical="12dp"
+ android:layout_marginBottom="16dp"
+ android:gravity="center_vertical">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
+ android:textColor="@color/primary_text"
+ android:text="@string/apptrackers_block_all_toggle"
+ />
+ <Switch
+ android:id="@+id/block_all_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="24dp"
+ android:checked="true"
+ />
+ </LinearLayout>
+
+ <include
+ android:id="@+id/disclaimer_block_trackers"
+ layout="@layout/disclaimer_block_trackers"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ />
+ <TextView
+ android:id="@+id/list_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/apptrackers_list_title"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:textSize="14sp"
+ android:lineHeight="24sp"
+ android:textFontWeight="500"
+ android:textColor="@color/primary_text"
+ />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_marginBottom="16dp"
+ tools:listitem="@layout/apptrackers_item_tracker_toggle"
+ />
+ <TextView
+ android:id="@+id/no_trackers_yet"
+ android:paddingHorizontal="32dp"
+ android:paddingTop="64dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ tools:text="@string/apptrackers_no_trackers_yet_block_off"
+ />
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/btn_reset"
+ style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
- android:layout_height="24dp"
- android:checked="true"
+ android:layout_height="wrap_content"
+ android:text="@string/apptrackers_reset"
+ android:textSize="14sp"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ tools:visibility="visible"
+ android:layout_margin="16dp"
/>
</LinearLayout>
- <View
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:background="@color/divider"
- />
- <TextView
- android:id="@+id/trackers_list_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="@string/apptrackers_trackers_list_title"
- android:padding="16dp"
- android:visibility="gone"
- />
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/trackers"
- android:layout_height="wrap_content"
- android:layout_width="match_parent"
- android:layout_marginBottom="16dp"
- tools:listitem="@layout/apptrackers_item_tracker_toggle"
- android:visibility="gone"
- />
- <TextView
- android:id="@+id/no_trackers_yet"
- android:paddingHorizontal="32dp"
- android:paddingTop="64dp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- tools:text="@string/apptrackers_no_trackers_yet_block_off"
- />
- <com.google.android.material.button.MaterialButton
- android:id="@+id/btn_reset"
- style="@style/Widget.MaterialComponents.Button.OutlinedButton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/apptrackers_reset"
- android:textSize="14sp"
- android:layout_gravity="center"
- android:visibility="gone"
- tools:visibility="visible"
- android:layout_margin="16dp"
- />
- </LinearLayout>
</androidx.core.widget.NestedScrollView>
- </androidx.coordinatorlayout.widget.CoordinatorLayout>
-</layout> \ No newline at end of file
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml b/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml
index db7086f..753e734 100644
--- a/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml
+++ b/app/src/main/res/layout/apptrackers_item_tracker_toggle.xml
@@ -1,12 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
- android:layout_height="52dp"
+ android:layout_height="62dp"
android:layout_width="match_parent"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
android:gravity="center_vertical"
>
<TextView
@@ -18,6 +32,8 @@
android:maxLines="1"
android:ellipsize="end"
android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
tools:text="Body sensor"
/>
<Switch
diff --git a/app/src/main/res/layout/disclaimer_block_trackers.xml b/app/src/main/res/layout/disclaimer_block_trackers.xml
new file mode 100644
index 0000000..5d58c5f
--- /dev/null
+++ b/app/src/main/res/layout/disclaimer_block_trackers.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_shield_alert"
+ android:drawableTint="@color/primary_text"
+ tools:text="Note: in some rare cases, disabling tracker can cause some apps to malfunction. You can choose specifically which trackers you want to block. Know more."
+ android:drawablePadding="16dp"
+ android:paddingHorizontal="16dp"
+ android:paddingVertical="12dp"
+ android:textSize="12sp"
+ android:lineHeight="16sp"
+ android:textFontWeight="400"
+ android:background="@drawable/bg_stroke_rounded_12" />
diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml
index 3c709e9..5da95e1 100644
--- a/app/src/main/res/layout/fragment_fake_location.xml
+++ b/app/src/main/res/layout/fragment_fake_location.xml
@@ -1,4 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 MURENA SAS
+ ~ Copyright (C) 2021 E FOUNDATION
+ ~
+ ~ This program is free software: you can redistribute it and/or modify
+ ~ 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/>.
+ -->
<layout>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -89,7 +106,7 @@
android:id="@+id/map_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="@color/grey_overlay"
+ android:background="@color/disabled"
android:visibility="visible"
/>
<ProgressBar
diff --git a/app/src/main/res/layout/fragment_trackers.xml b/app/src/main/res/layout/fragment_trackers.xml
index 0cd5980..683f01c 100644
--- a/app/src/main/res/layout/fragment_trackers.xml
+++ b/app/src/main/res/layout/fragment_trackers.xml
@@ -1,4 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 MURENA SAS
+ ~ Copyright (C) 2021 E FOUNDATION
+ ~
+ ~ This program is free software: you can redistribute it and/or modify
+ ~ 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/>.
+ -->
<layout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
@@ -13,14 +30,9 @@
<androidx.core.widget.NestedScrollView
android:layout_height="match_parent"
android:layout_width="match_parent"
+ android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
-
-<LinearLayout
- android:layout_height="match_parent"
- android:layout_width="match_parent"
- android:orientation="vertical"
- >
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
@@ -66,24 +78,37 @@
app:period="@{@string/trackers_period_year}"
/>
<TextView
- android:id="@+id/trackers_apps_list_title"
+ android:id="@+id/trackers_lists_title"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_marginTop="32dp"
- android:paddingTop="16dp"
- android:paddingHorizontal="16dp"
- android:text="@string/trackers_applist_title"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginBottom="24dp"
+ android:textSize="14sp"
+ android:lineHeight="24dp"
+ android:textFontWeight="500"
+ android:text="@string/trackers_lists_title"
+ />
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/lists_tabs"
+ android:layout_width="match_parent"
+ android:layout_height="32dp"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginBottom="32dp"
+ android:background="@drawable/pill_shape_tab_bg"
+ app:tabBackground="@drawable/pill_shape_tab_indicator"
+ app:tabIndicator="@null"
+ app:tabSelectedTextColor="@color/white"
+ app:tabTextColor="@color/primary_text"
+ app:textAllCaps="false"
/>
- </LinearLayout>
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/apps"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="16dp"
- android:paddingBottom="32dp"
- />
-</LinearLayout>
-</androidx.core.widget.NestedScrollView>
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/lists_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ />
+ </LinearLayout>
+ </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
-</layout> \ No newline at end of file
+</layout>
diff --git a/app/src/main/res/layout/highlight_data_number.xml b/app/src/main/res/layout/highlight_data_number.xml
new file mode 100644
index 0000000..7938165
--- /dev/null
+++ b/app/src/main/res/layout/highlight_data_number.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="12dp"
+ android:gravity="center_horizontal"
+ android:background="@drawable/bg_stroke_rounded_12">
+ <TextView
+ android:id="@+id/primary_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
+ android:textColor="@color/primary_text"
+ android:textAllCaps="true"
+ tools:text="Detected in"
+ />
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="8dp"
+ android:textSize="24sp"
+ android:lineHeight="24sp"
+ android:textFontWeight="400"
+ android:textColor="@color/primary_text"
+ tools:text="5"
+ />
+ <TextView
+ android:id="@+id/secondary_message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="12sp"
+ android:lineHeight="16sp"
+ android:textFontWeight="400"
+ android:textColor="@color/disabled"
+ tools:text="Different applications"
+ />
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/trackerdetails_fragment.xml b/app/src/main/res/layout/trackerdetails_fragment.xml
new file mode 100644
index 0000000..45ba0e4
--- /dev/null
+++ b/app/src/main/res/layout/trackerdetails_fragment.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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/>.
+ -->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+
+ android:background="@color/background"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ >
+
+ <include layout="@layout/topbar" />
+
+ <androidx.core.widget.NestedScrollView
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:fillViewport="true"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"
+ >
+
+ <LinearLayout
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="vertical"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="32dp"
+ >
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
+ android:textColor="@color/disabled"
+ android:layout_marginBottom="16dp"
+ tools:text="Doubleclic.net tracking summary"
+ />
+ <include
+ android:id="@+id/data_app_count"
+ layout="@layout/highlight_data_number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ />
+
+ <include
+ android:id="@+id/data_blocked_leaks"
+ layout="@layout/highlight_data_number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="14sp"
+ android:lineHeight="24sp"
+ android:textFontWeight="500"
+ android:textColor="@color/primary_text"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:text="@string/trackerdetails_manage_tracker"
+ />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingVertical="12dp"
+ android:layout_marginBottom="16dp"
+ android:gravity="center_vertical">
+ <TextView
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
+ android:textColor="@color/primary_text"
+ android:text="@string/trackerdetails_block_all_toggle"
+ />
+ <Switch
+ android:id="@+id/block_all_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="24dp"
+ android:checked="true"
+ />
+ </LinearLayout>
+
+ <include
+ android:id="@+id/disclaimer_block_trackers"
+ layout="@layout/disclaimer_block_trackers"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ />
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/trackerdetails_apps_list_title"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="16dp"
+ android:textSize="14sp"
+ android:lineHeight="24sp"
+ android:textFontWeight="500"
+ android:textColor="@color/primary_text"
+ />
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/apps"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_marginBottom="16dp"
+ tools:listitem="@layout/apptrackers_item_tracker_toggle"
+
+ />
+ </LinearLayout>
+ </androidx.core.widget.NestedScrollView>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/trackers_item_app.xml b/app/src/main/res/layout/trackers_item_app.xml
index 6af43ea..883a4da 100644
--- a/app/src/main/res/layout/trackers_item_app.xml
+++ b/app/src/main/res/layout/trackers_item_app.xml
@@ -1,20 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
- android:layout_height="52dp"
+ android:layout_height="60dp"
android:layout_width="match_parent"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
+ android:paddingHorizontal="24dp"
+ android:paddingVertical="12dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground"
>
<ImageView
android:id="@+id/icon"
- android:layout_height="32dp"
- android:layout_width="32dp"
+ android:layout_height="24dp"
+ android:layout_width="24dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -24,10 +40,12 @@
android:id="@+id/title"
android:layout_height="wrap_content"
android:layout_width="0dp"
+ android:layout_marginStart="16dp"
android:maxLines="1"
android:ellipsize="end"
- android:layout_marginStart="16dp"
android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:textFontWeight="400"
android:textColor="@color/primary_text"
tools:text="Body sensor"
app:layout_constraintLeft_toRightOf="@+id/icon"
@@ -42,20 +60,22 @@
android:layout_width="0dp"
android:maxLines="1"
android:ellipsize="end"
- android:layout_marginStart="16dp"
- android:textSize="14sp"
- android:textColor="@color/secondary_text"
- tools:text="1 tracker blocked out of 4"
- app:layout_constraintLeft_toRightOf="@+id/icon"
+ android:textSize="12sp"
+ android:lineHeight="16sp"
+ android:textFontWeight="400"
+ android:textColor="@color/disabled"
+ tools:text="5 trackers detected"
+ app:layout_constraintLeft_toLeftOf="@+id/title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title"
- app:layout_constraintRight_toLeftOf="@+id/arrow"
+ app:layout_constraintRight_toRightOf="@+id/title"
/>
<ImageView
android:id="@+id/arrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_chevron_right_24dp"
+ android:layout_marginStart="16dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/app/src/main/res/layout/trackers_list.xml b/app/src/main/res/layout/trackers_list.xml
new file mode 100644
index 0000000..3191b91
--- /dev/null
+++ b/app/src/main/res/layout/trackers_list.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 MURENA SAS
+ ~ Copyright (C) 2021 E FOUNDATION
+ ~
+ ~ This program is free software: you can redistribute it and/or modify
+ ~ 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/>.
+ -->
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+</androidx.recyclerview.widget.RecyclerView>
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 52a1677..1047da6 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -61,6 +61,9 @@
android:id="@+id/goto_appTrackersFragment"
app:destination="@id/appTrackersFragment"
/>
+ <action
+ android:id="@+id/goto_trackerDetailsFragment"
+ app:destination="@id/trackerDetailsFragment" />
</fragment>
<fragment
android:id="@+id/appTrackersFragment"
@@ -74,6 +77,14 @@
/>
</fragment>
<fragment
+ android:id="@+id/trackerDetailsFragment"
+ android:name="foundation.e.advancedprivacy.features.trackers.trackerdetails.TrackerDetailsFragment"
+ android:label="TrackerDetailsFragment">
+ <argument
+ android:name="trackerId"
+ app:argType="string" />
+ </fragment>
+ <fragment
android:id="@+id/fakeLocationFragment"
android:name="foundation.e.advancedprivacy.features.location.FakeLocationFragment"
android:label="@string/location_title"
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 952ed60..e0e9530 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,4 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 MURENA SAS
+ ~ Copyright (C) 2021 E FOUNDATION
+ ~
+ ~ This program is free software: you can redistribute it and/or modify
+ ~ 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/>.
+ -->
<resources>
<!--themable -->
<color name="primary">@color/e_action_bar</color>
@@ -10,14 +27,16 @@
<color name="background">@color/e_background</color>
<color name="iconInvertedColor">@color/e_alpha_base</color>
+ <color name="disabled">@color/e_disabled_color</color>
+ <color name="divider">@color/e_divider_color</color>
+
+ <color name="background_overlay">@color/e_background_overlay</color>
<!-- Custom -->
<color name="dark_color">#263238</color>
<color name="white">#FFFFFFFF</color>
- <color name="grey_overlay">@color/e_disabled_color</color>
<color name="green_valid">#28C97C</color>
<color name="red_off">#F8432E</color>
<color name="blue_unselected">#AADCFE</color> <!-- used in combination with accent color ! -->
- <color name="divider">@color/e_background_overlay</color>
<!-- Widget -->
<color name="on_surface_high_emphasis">@color/e_primary_text_color_dark</color>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ba3ba03..aa33837 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,4 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 MURENA SAS
+ ~ Copyright (C) 2021 E FOUNDATION
+ ~
+ ~ This program is free software: you can redistribute it and/or modify
+ ~ 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/>.
+ -->
<resources>
<string name="app_name" translatable="false">Advanced Privacy</string>
@@ -80,25 +97,55 @@
<string name="trackers_period_day" weblate_ctx="trackers-1">24 hours</string>
<string name="trackers_period_month" weblate_ctx="trackers-1;trackers-2">past month</string>
<string name="trackers_period_year" weblate_ctx="trackers-1">past year</string>
- <string name="trackers_applist_title" weblate_ctx="trackers-3">Manage trackers used in applications * :</string>
+
<string name="trackers_applist_infos" translatable="false">@string/ipscrambling_app_list_infos</string>
+ <string name="trackers_lists_title" weblate_ctx="trackers-3">Trackers Activity Summary</string>
+ <string name="trackers_toggle_list_apps">Apps</string>
+ <string name="trackers_toggle_list_trackers">Trackers</string>
+
+ <string name="trackers_list_app_trackers_counts">%s trackers detected</string>
+ <string name="trackers_list_tracker_apps_counts">detected in %s apps</string>
+
<string name="trackers_graph_hours_period_format">HH:mm</string>
<string name="trackers_graph_days_period_format">MMMM d - EEE</string>
<string name="trackers_graph_months_period_format">MMMM yyyy</string>
- <string name="trackers_app_trackers_counts">%1$d/%2$d blocked trackers, %3$d leaks</string>
+
<!-- App Trackers -->
- <string name="apptrackers_block_all_toggle">Block trackers</string>
- <string name="apptrackers_trackers_list_title">Opt for the trackers you want to activate/deactivate.</string>
+ <string name="apptrackers_subtitle">%s tracking summary</string>
+ <string name="apptrackers_detected_tracker_primary">Total</string>
+ <string name="apptrackers_detected_tracker_secondary">Detected trackers</string>
+ <string name="apptrackers_blocked_tracker_primary">Blocked</string>
+ <string name="apptrackers_blocked_tracker_secondary">Trackers</string>
+ <string name="apptrackers_blocked_leaks_primary">Blocked leaks</string>
+ <string name="apptrackers_blocked_leaks_secondary">%s allowed leaks</string>
+ <string name="apptrackers_manage_tracker">Manage tracker</string>
+ <string name="apptrackers_block_all_toggle">Toggle on trackers control</string>
+ <string name="apptrackers_list_title">Toggle off the trackers you want to allow:</string>
<string name="apptrackers_no_trackers_yet_block_off">No trackers were detected yet. If new trackers are detected they will be updated here.</string>
<string name="apptrackers_no_trackers_yet_block_on">No trackers were detected yet. All future trackers will be blocked.</string>
<string name="app_trackers_no_trackers_yet_remaining_whitelist">No trackers were detected yet. Some trackers were unblocked previously.</string>
<string name="apptrackers_error_quickprivacy_disabled">Enable Quick Privacy to be able to activate/deactivate trackers.</string>
- <string name="apptrackers_trackers_count_summary">%1$d blocked trackers out of %2$d detected trackers, %3$d blocked leaks and %4$d allowed leaks.</string>
<string name="apptrackers_error_no_app">App not installed.</string>
<string name="apptrackers_tracker_control_disabled_message">Changes will take effect when tracker blocker is on.</string>
<string name="apptrackers_reset">Reset trackers</string>
+ <!-- Tracker details -->
+ <string name="trackerdetails_subtitle">%s tracking summary</string>
+ <string name="trackerdetails_app_count_primary">Detected in</string>
+ <string name="trackerdetails_app_count_secondary">Different applications</string>
+ <string name="trackerdetails_blocked_leaks_primary">Blocked leaks</string>
+ <string name="trackerdetails_blocked_leaks_secondary">%s allowed leaks</string>
+ <string name="trackerdetails_manage_tracker">Manage tracker</string>
+ <string name="trackerdetails_block_all_toggle">Block this tracker across all apps</string>
+ <string name="trackerdetails_apps_list_title">Toggle off the apps for which you want to allow this tracker:</string>
+
+ <!-- Trackers control disclaimer block -->
+ <string name="trackercontroldisclaimer_start">Note:</string>
+ <string name="trackercontroldisclaimer_body">in some rare cases, disabling tracker can cause some apps to malfunction. You can choose specifically which trackers you want to block.</string>
+ <string name="trackercontroldisclaimer_link">Know more.</string>
+
+
<!-- Features warning dialog -->
<string name="warningdialog_do_not_show_again" weblate_ctx="home-trackers-alert;widget-trackers-alert;home-location-alert;widget-location-alert;home-ipscrambling-alert;widget-ipscrambling-alert">Do not show again</string>
<string name="warningdialog_trackers_title" weblate_ctx="home-trackers-alert;widget-trackers-alert">Trackers control</string>
diff --git a/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt b/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt
index f29bb8a..b44e96e 100644
--- a/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt
+++ b/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
class AppListsRepository(
private val permissionsModule: IPermissionsPrivacyModule,
@@ -179,20 +180,24 @@ class AppListsRepository(
} else test(app)
}
- fun applyForHiddenApps(app: ApplicationDescription, action: (ApplicationDescription) -> Unit) {
+ fun getRealApps(app: ApplicationDescription): List<ApplicationDescription> {
+ return when (app) {
+ dummySystemApp -> getHiddenSystemApps()
+ dummyCompatibilityApp -> getCompatibilityApps()
+ else -> listOf(app)
+ }
+ }
+
+ suspend fun applyForHiddenApps(app: ApplicationDescription, action: suspend (ApplicationDescription) -> Unit) {
mapReduceForHiddenApps(app = app, map = action, reduce = {})
}
- fun <T, R> mapReduceForHiddenApps(
+ suspend fun <T, R> mapReduceForHiddenApps(
app: ApplicationDescription,
- map: (ApplicationDescription) -> T,
- reduce: (List<T>) -> R
+ map: suspend (ApplicationDescription) -> T,
+ reduce: suspend (List<T>) -> R
): R {
- return if (app == dummySystemApp) {
- reduce(getHiddenSystemApps().map(map))
- } else if (app == dummyCompatibilityApp) {
- reduce(getCompatibilityApps().map(map))
- } else reduce(listOf(map(app)))
+ return reduce(getRealApps(app).map { map(it) })
}
private var appsByUid = mapOf<Int, ApplicationDescription>()
@@ -214,6 +219,16 @@ class AppListsRepository(
}
}
+ suspend fun getDisplayableApp(apId: String): ApplicationDescription? = withContext(Dispatchers.IO) {
+ getApp(apId)?.let { app ->
+ when {
+ app in getCompatibilityApps() -> dummyCompatibilityApp
+ app in getHiddenSystemApps() -> dummySystemApp
+ else -> app
+ }
+ }
+ }
+
private val allProfilesAppDescriptions = MutableStateFlow(
Triple(
emptyList<ApplicationDescription>(),
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5c945fa..0bb32ba 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -28,6 +28,7 @@ androidx-room-common = { group = "androidx.room", name = "room-common", version.
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidx-room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
+androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version = "1.0.0" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version = "2.7.1" }
eos-elib = { group = "foundation.e", name = "elib", version = "0.0.1-alpha11" }
eos-orbotservice = { group = "foundation.e", name = "orbotservice", version.ref = "orbotservice" }
diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt
index 15ff813..a80d4dc 100644
--- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt
+++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/StatsDatabase.kt
@@ -224,61 +224,112 @@ class StatsDatabase(
}
}
- fun getContactedTrackersCountByAppId(): Map<String, Int> {
+ fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> {
synchronized(lock) {
+ val minTimestamp = getPeriodStartTs(periodCount, periodUnit)
val db = readableDatabase
- val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER"
+ val selection = "$COLUMN_NAME_APPID = ? AND " +
+ "$COLUMN_NAME_TIMESTAMP >= ?"
+ val selectionArg = arrayOf("" + appId, "" + minTimestamp)
+ val projection =
+ "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," +
+ "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM"
val cursor = db.rawQuery(
- "SELECT DISTINCT $projection FROM $TABLE_NAME", // +
- arrayOf()
+ "SELECT $projection FROM $TABLE_NAME WHERE $selection",
+ selectionArg
)
- val countByApp = mutableMapOf<String, Int>()
- while (cursor.moveToNext()) {
- trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let {
- val appId = cursor.getString(COLUMN_NAME_APPID)
- countByApp[appId] = countByApp.getOrDefault(appId, 0) + 1
- }
+ var calls: Pair<Int, Int> = 0 to 0
+ if (cursor.moveToNext()) {
+ val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM)
+ val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM)
+ calls = blocked to contacted - blocked
}
cursor.close()
db.close()
- return countByApp
+ return calls
}
}
- fun getCallsByAppIds(periodCount: Int, periodUnit: TemporalUnit): Map<String, Pair<Int, Int>> {
+ fun getMostLeakedAppId(periodCount: Int, periodUnit: TemporalUnit): String {
synchronized(lock) {
val minTimestamp = getPeriodStartTs(periodCount, periodUnit)
val db = readableDatabase
val selection = "$COLUMN_NAME_TIMESTAMP >= ?"
val selectionArg = arrayOf("" + minTimestamp)
val projection = "$COLUMN_NAME_APPID, " +
- "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," +
- "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM"
+ "SUM($COLUMN_NAME_NUMBER_CONTACTED - $COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_LEAKED_SUM"
val cursor = db.rawQuery(
"SELECT $projection FROM $TABLE_NAME" +
" WHERE $selection" +
- " GROUP BY $COLUMN_NAME_APPID",
+ " GROUP BY $COLUMN_NAME_APPID" +
+ " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1",
selectionArg
)
- val callsByApp = HashMap<String, Pair<Int, Int>>()
+ var appId = ""
+ if (cursor.moveToNext()) {
+ appId = cursor.getString(COLUMN_NAME_APPID)
+ }
+ cursor.close()
+ db.close()
+ return appId
+ }
+ }
+
+ fun getDistinctTrackerAndApp(): List<Pair<String, String>> {
+ synchronized(lock) {
+ val db = readableDatabase
+ val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER"
+ val cursor = db.rawQuery(
+ "SELECT DISTINCT $projection FROM $TABLE_NAME", // +
+ arrayOf()
+ )
+
+ val res = mutableListOf<Pair<String, String>>()
while (cursor.moveToNext()) {
- val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM)
- val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM)
- callsByApp[cursor.getString(COLUMN_NAME_APPID)] = blocked to contacted - blocked
+ res.add(
+ cursor.getString(COLUMN_NAME_TRACKER) to cursor.getString(COLUMN_NAME_APPID)
+ )
}
cursor.close()
db.close()
- return callsByApp
+ return res
}
}
- fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> {
+ suspend fun getApIds(trackerId: String): List<String> = withContext(Dispatchers.IO) {
synchronized(lock) {
- val minTimestamp = getPeriodStartTs(periodCount, periodUnit)
val db = readableDatabase
- val selection = "$COLUMN_NAME_APPID = ? AND " +
- "$COLUMN_NAME_TIMESTAMP >= ?"
- val selectionArg = arrayOf("" + appId, "" + minTimestamp)
+ val columns = arrayOf(COLUMN_NAME_APPID, COLUMN_NAME_TRACKER)
+ val selection = "$COLUMN_NAME_TRACKER = ?"
+ val selectionArg = arrayOf(trackerId)
+ val cursor = db.query(
+ true,
+ TABLE_NAME,
+ columns,
+ selection,
+ selectionArg,
+ null,
+ null,
+ null,
+ null
+ )
+
+ val apIds: MutableList<String> = ArrayList()
+ while (cursor.moveToNext()) {
+ apIds.add(cursor.getString(COLUMN_NAME_APPID))
+ }
+ cursor.close()
+ db.close()
+
+ apIds
+ }
+ }
+
+ suspend fun getCallsForApp(apId: String): Pair<Int, Int> = withContext(Dispatchers.IO) {
+ synchronized(lock) {
+ val db = readableDatabase
+ val selection = "$COLUMN_NAME_APPID = ?"
+ val selectionArg = arrayOf(apId)
val projection =
"SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," +
"SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM"
@@ -294,32 +345,31 @@ class StatsDatabase(
}
cursor.close()
db.close()
- return calls
+ calls
}
}
- fun getMostLeakedAppId(periodCount: Int, periodUnit: TemporalUnit): String {
+ suspend fun getCallsForTracker(trackerId: String): Pair<Int, Int> = withContext(Dispatchers.IO) {
synchronized(lock) {
- val minTimestamp = getPeriodStartTs(periodCount, periodUnit)
val db = readableDatabase
- val selection = "$COLUMN_NAME_TIMESTAMP >= ?"
- val selectionArg = arrayOf("" + minTimestamp)
- val projection = "$COLUMN_NAME_APPID, " +
- "SUM($COLUMN_NAME_NUMBER_CONTACTED - $COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_LEAKED_SUM"
+ val selection = "$COLUMN_NAME_TRACKER = ?"
+ val selectionArg = arrayOf(trackerId)
+ val projection =
+ "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," +
+ "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM"
val cursor = db.rawQuery(
- "SELECT $projection FROM $TABLE_NAME" +
- " WHERE $selection" +
- " GROUP BY $COLUMN_NAME_APPID" +
- " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1",
+ "SELECT $projection FROM $TABLE_NAME WHERE $selection",
selectionArg
)
- var appId = ""
+ var calls: Pair<Int, Int> = 0 to 0
if (cursor.moveToNext()) {
- appId = cursor.getString(COLUMN_NAME_APPID)
+ val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM)
+ val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM)
+ calls = blocked to contacted - blocked
}
cursor.close()
db.close()
- return appId
+ calls
}
}
@@ -386,7 +436,40 @@ class StatsDatabase(
return entry
}
- fun getTrackers(appIds: List<String>?): List<Tracker> {
+ suspend fun getTrackerIds(appIds: List<String>?): List<String> = withContext(Dispatchers.IO) {
+ synchronized(lock) {
+ val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID)
+ var selection: String? = null
+
+ var selectionArg: Array<String>? = null
+ appIds?.let { appIds ->
+ selection = "$COLUMN_NAME_APPID IN (${appIds.joinToString(", ") { "'$it'" }})"
+ selectionArg = arrayOf()
+ }
+
+ val db = readableDatabase
+ val cursor = db.query(
+ true,
+ TABLE_NAME,
+ columns,
+ selection,
+ selectionArg,
+ null,
+ null,
+ null,
+ null
+ )
+ val trackerIds: MutableList<String> = mutableListOf()
+ while (cursor.moveToNext()) {
+ trackerIds.add(cursor.getString(COLUMN_NAME_TRACKER))
+ }
+ cursor.close()
+ db.close()
+ trackerIds
+ }
+ }
+
+ suspend fun getTrackers(appIds: List<String>?): List<Tracker> = withContext(Dispatchers.IO) {
synchronized(lock) {
val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID)
var selection: String? = null
@@ -419,7 +502,7 @@ class StatsDatabase(
}
cursor.close()
db.close()
- return trackers
+ trackers
}
}
diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt
index 429c5e9..9f37a1d 100644
--- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt
+++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/WhitelistRepository.kt
@@ -23,6 +23,9 @@ import android.content.SharedPreferences
import foundation.e.advancedprivacy.data.repositories.AppListsRepository
import foundation.e.advancedprivacy.domain.entities.ApplicationDescription
import foundation.e.advancedprivacy.trackers.domain.entities.Tracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
import java.io.File
class WhitelistRepository(
@@ -32,18 +35,25 @@ class WhitelistRepository(
private var appsWhitelist: Set<String> = HashSet()
private var appUidsWhitelist: Set<Int> = HashSet()
- private var trackersWhitelistByApp: MutableMap<String, MutableSet<String>> = HashMap()
- private var trackersWhitelistByUid: Map<Int, MutableSet<String>> = HashMap()
+ private var trackersWhitelist: Set<String> = HashSet()
+
+ private var apIdTrackersWhitelist: Map<String, Boolean> = emptyMap()
+ private var appUidTrackersWhitelist: Map<String, Boolean> = emptyMap()
private val prefs: SharedPreferences
companion object {
- private const val SHARED_PREFS_FILE = "trackers_whitelist_v2"
+ private const val SHARED_PREFS_FILE = "trackers_whitelist_v3"
private const val KEY_BLOCKING_ENABLED = "blocking_enabled"
private const val KEY_APPS_WHITELIST = "apps_whitelist"
- private const val KEY_APP_TRACKERS_WHITELIST_PREFIX = "app_trackers_whitelist_"
+ private const val KEY_TRACKERS_WHITELIST = "trackers_whitelist"
+ private const val KEY_APP_TRACKER_WHITELIST = "app_tracker_whitelist"
+ private const val KEY_APP_TRACKER_BLACKLIST = "app_tracker_blacklist"
+ // Deprecated keys.
private const val SHARED_PREFS_FILE_V1 = "trackers_whitelist.prefs"
+ private const val SHARED_PREFS_FILE_V2 = "trackers_whitelist_v2"
+ private const val KEY_APP_TRACKERS_WHITELIST_PREFIX = "app_trackers_whitelist_"
}
init {
@@ -56,6 +66,9 @@ class WhitelistRepository(
if (context.sharedPreferencesExists(SHARED_PREFS_FILE_V1)) {
migrate1To2(context)
}
+ if (context.sharedPreferencesExists(SHARED_PREFS_FILE_V2)) {
+ migrate2To3(context)
+ }
}
private fun Context.sharedPreferencesExists(fileName: String): Boolean {
@@ -86,7 +99,7 @@ class WhitelistRepository(
val apId = appListsRepository.getApp(uid)?.apId
apId?.let {
val trackers = prefsV1.getStringSet(key, emptySet())
- editorV2.putStringSet(buildAppTrackersKey(apId), trackers)
+ editorV2.putStringSet(KEY_APP_TRACKERS_WHITELIST_PREFIX + apId, trackers)
}
} catch (e: Exception) { }
}
@@ -98,10 +111,39 @@ class WhitelistRepository(
reloadCache()
}
+ private fun migrate2To3(context: Context) {
+ val prefsV2 = context.getSharedPreferences(SHARED_PREFS_FILE_V1, Context.MODE_PRIVATE)
+ val editorV3 = prefs.edit()
+
+ editorV3.putBoolean(KEY_BLOCKING_ENABLED, prefsV2.getBoolean(KEY_BLOCKING_ENABLED, false))
+
+ prefsV2.getStringSet(KEY_APPS_WHITELIST, null)?.let {
+ editorV3.putStringSet(KEY_APPS_WHITELIST, it)
+ }
+ editorV3.commit()
+
+ runBlocking {
+ prefsV2.all.keys.forEach { key ->
+ if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) {
+ runCatching {
+ val apId = key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length)
+ prefsV2.getStringSet(key, null)
+ ?.map { trackerId -> buildApIdTrackerKey(apId, trackerId) }
+ ?.let { setWhitelisted(it, true) }
+ }
+ }
+ }
+ }
+
+ context.deleteSharedPreferences(SHARED_PREFS_FILE_V2)
+
+ reloadCache()
+ }
+
private fun reloadCache() {
isBlockingEnabled = prefs.getBoolean(KEY_BLOCKING_ENABLED, false)
reloadAppsWhiteList()
- reloadAllAppTrackersWhiteList()
+ reloadAppTrackersWhitelist()
}
private fun reloadAppsWhiteList() {
@@ -111,24 +153,28 @@ class WhitelistRepository(
.toSet()
}
- private fun refreshAppUidTrackersWhiteList() {
- trackersWhitelistByUid = trackersWhitelistByApp.mapNotNull { (apId, value) ->
+ private fun reloadTrackersWhiteList() {
+ trackersWhitelist = prefs.getStringSet(KEY_TRACKERS_WHITELIST, HashSet()) ?: HashSet()
+ }
+
+ private fun reloadAppTrackersWhitelist() {
+ val whitelist = mutableMapOf<String, Boolean>()
+ prefs.getStringSet(KEY_APP_TRACKER_WHITELIST, HashSet())?.forEach { key ->
+ whitelist[key] = true
+ }
+
+ prefs.getStringSet(KEY_APP_TRACKER_BLACKLIST, HashSet())?.forEach { key ->
+ whitelist[key] = false
+ }
+
+ apIdTrackersWhitelist = whitelist
+ appUidTrackersWhitelist = whitelist.mapNotNull { (apIdTrackerId, isWhitelisted) ->
+ val (apId, tracker) = parseApIdTrackerKey(apIdTrackerId)
appListsRepository.getApp(apId)?.uid?.let { uid ->
- uid to value
+ buildAppUidTrackerKey(uid, tracker) to isWhitelisted
}
}.toMap()
}
- private fun reloadAllAppTrackersWhiteList() {
- val map: MutableMap<String, MutableSet<String>> = HashMap()
- prefs.all.keys.forEach { key ->
- if (key.startsWith(KEY_APP_TRACKERS_WHITELIST_PREFIX)) {
- map[key.substring(KEY_APP_TRACKERS_WHITELIST_PREFIX.length)] = (
- prefs.getStringSet(key, HashSet()) ?: HashSet()
- )
- }
- }
- trackersWhitelistByApp = map
- }
var isBlockingEnabled: Boolean = false
get() = field
@@ -149,34 +195,83 @@ class WhitelistRepository(
reloadAppsWhiteList()
}
- private fun buildAppTrackersKey(apId: String): String {
- return KEY_APP_TRACKERS_WHITELIST_PREFIX + apId
- }
+ private suspend fun setWhitelisted(keys: List<String>, isWhitelisted: Boolean) = withContext(Dispatchers.IO) {
+ val whitelist = HashSet<String>().apply {
+ prefs.getStringSet(KEY_APP_TRACKER_WHITELIST, HashSet())?.let { addAll(it) }
+ }
- fun setWhiteListed(tracker: Tracker, apId: String, isWhiteListed: Boolean) {
- val trackers = trackersWhitelistByApp.getOrDefault(apId, HashSet())
- trackersWhitelistByApp[apId] = trackers
+ val blacklist = HashSet<String>().apply {
+ prefs.getStringSet(KEY_APP_TRACKER_BLACKLIST, HashSet())?.let { addAll(it) }
+ }
- if (isWhiteListed) {
- trackers.add(tracker.id)
+ if (isWhitelisted) {
+ blacklist.removeAll(keys)
+ whitelist.addAll(keys)
} else {
- trackers.remove(tracker.id)
+ whitelist.removeAll(keys)
+ blacklist.addAll(keys)
}
- refreshAppUidTrackersWhiteList()
- prefs.edit().putStringSet(buildAppTrackersKey(apId), trackers).commit()
+
+ prefs.edit().apply {
+ putStringSet(KEY_APP_TRACKER_BLACKLIST, blacklist)
+ putStringSet(KEY_APP_TRACKER_WHITELIST, whitelist)
+ commit()
+ }
+ reloadAppTrackersWhitelist()
+ }
+
+ suspend fun setWhiteListed(tracker: Tracker, apId: String, isWhitelisted: Boolean) {
+ setWhitelisted(listOf(buildApIdTrackerKey(apId, tracker.id)), isWhitelisted)
+ }
+
+ suspend fun setWhitelistedTrackersForApp(apId: String, trackerIds: List<String>, isWhitelisted: Boolean) = withContext(
+ Dispatchers.IO
+ ) {
+ setWhitelisted(
+ trackerIds.map { trackerId -> buildApIdTrackerKey(apId, trackerId) }, isWhitelisted
+ )
+ }
+
+ suspend fun setWhitelistedAppsForTracker(apIds: List<String>, trackerId: String, isWhitelisted: Boolean) = withContext(
+ Dispatchers.IO
+ ) {
+ setWhitelisted(
+ apIds.map { apId -> buildApIdTrackerKey(apId, trackerId) },
+ isWhitelisted
+ )
}
fun isAppWhiteListed(app: ApplicationDescription): Boolean {
return appsWhitelist.contains(app.apId)
}
- fun isWhiteListed(appUid: Int, trackerId: String?): Boolean {
- return appUidsWhitelist.contains(appUid) ||
- trackersWhitelistByUid.getOrDefault(appUid, HashSet()).contains(trackerId)
+ fun isAppWhiteListed(appUid: Int): Boolean {
+ return appUidsWhitelist.contains(appUid)
+ }
+
+ fun isWhiteListed(appUid: Int, trackerId: String?): Boolean? {
+ trackerId ?: return null
+
+ val key = buildAppUidTrackerKey(appUid, trackerId)
+ return appUidTrackersWhitelist.get(key)
+ }
+
+ private fun buildApIdTrackerKey(apId: String, trackerId: String): String {
+ return "$apId|$trackerId"
+ }
+
+ private fun parseApIdTrackerKey(key: String): Pair<String, String> {
+ return key.split("|").let { it[0] to it[1] }
+ }
+
+ private fun buildAppUidTrackerKey(appUid: Int, trackerId: String): String {
+ return "$appUid-$trackerId"
}
fun areWhiteListEmpty(): Boolean {
- return appsWhitelist.isEmpty() && trackersWhitelistByApp.all { (_, trackers) -> trackers.isEmpty() }
+ return appsWhitelist.isEmpty() &&
+ trackersWhitelist.isEmpty() &&
+ apIdTrackersWhitelist.values.none { it }
}
fun getWhiteListedApp(): List<ApplicationDescription> {
@@ -184,12 +279,64 @@ class WhitelistRepository(
}
fun getWhiteListForApp(app: ApplicationDescription): List<String> {
- return trackersWhitelistByApp[app.apId]?.toList() ?: emptyList()
+ return apIdTrackersWhitelist.entries.mapNotNull { (key, isWhitelisted) ->
+ if (!isWhitelisted) {
+ null
+ } else {
+ val (apId, tracker) = parseApIdTrackerKey(key)
+ if (apId == app.apId) {
+ tracker
+ } else {
+ null
+ }
+ }
+ }
}
fun clearWhiteList(apId: String) {
- trackersWhitelistByApp.remove(apId)
- refreshAppUidTrackersWhiteList()
- prefs.edit().remove(buildAppTrackersKey(apId)).commit()
+ val (whitelistToRemove, blacklistToRemove) = apIdTrackersWhitelist.entries
+ .filter { (key, _) -> key.startsWith(apId) }
+ .partition { (_, whitelisted) -> whitelisted }.let { (whitelistEntries, blacklistEntries) ->
+ whitelistEntries.map { it.key }.toSet() to
+ blacklistEntries.map { it.key }.toSet()
+ }
+
+ val whitelist = HashSet<String>().apply {
+ prefs.getStringSet(KEY_APP_TRACKER_WHITELIST, HashSet())?.let { addAll(it) }
+ }
+
+ val blacklist = HashSet<String>().apply {
+ prefs.getStringSet(KEY_APP_TRACKER_BLACKLIST, HashSet())?.let { addAll(it) }
+ }
+
+ whitelist.removeAll(whitelistToRemove)
+ blacklist.removeAll(blacklistToRemove)
+
+ prefs.edit().apply {
+ putStringSet(KEY_APP_TRACKER_WHITELIST, whitelist)
+ putStringSet(KEY_APP_TRACKER_BLACKLIST, blacklist)
+ commit()
+ }
+ reloadAppTrackersWhitelist()
+ }
+
+ fun setWhiteListed(tracker: Tracker, isWhiteListed: Boolean) {
+ val current = prefs.getStringSet(KEY_TRACKERS_WHITELIST, HashSet())?.toHashSet() ?: HashSet()
+
+ if (isWhiteListed) {
+ current.add(tracker.id)
+ } else {
+ current.remove(tracker.id)
+ }
+ prefs.edit().putStringSet(KEY_TRACKERS_WHITELIST, current).commit()
+ reloadTrackersWhiteList()
+ }
+
+ fun isWhiteListed(tracker: Tracker): Boolean {
+ return trackersWhitelist.contains(tracker.id)
+ }
+
+ fun isTrackerWhiteListed(trackerId: String): Boolean {
+ return trackersWhitelist.contains(trackerId)
}
}
diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt
index e229cab..e0fae43 100644
--- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt
+++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/FilterHostnameUseCase.kt
@@ -81,7 +81,15 @@ class FilterHostnameUseCase(
private fun shouldBlock(appUid: Int, trackerId: String?): Boolean {
return whitelistRepository.isBlockingEnabled &&
- !whitelistRepository.isWhiteListed(appUid, trackerId)
+ trackerId != null &&
+ !isWhitelisted(appUid, trackerId)
+ }
+
+ fun isWhitelisted(appUid: Int, trackerId: String): Boolean {
+ return whitelistRepository.isWhiteListed(appUid, trackerId) ?: (
+ whitelistRepository.isTrackerWhiteListed(trackerId) ||
+ whitelistRepository.isAppWhiteListed(appUid)
+ )
}
private val queue = LinkedBlockingQueue<DetectedTracker>()
diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt
index e7a84b8..22bd8fc 100644
--- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt
+++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt
@@ -39,25 +39,14 @@ class StatisticsUseCase(
return database.getActiveTrackersByPeriod(periodsCount, periodUnit)
}
- fun getContactedTrackersCountByApp(): Map<ApplicationDescription, Int> {
- return database.getContactedTrackersCountByAppId().mapByAppIdToApp()
- }
-
fun getContactedTrackersCount(): Int {
return database.getContactedTrackersCount()
}
- fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> {
+ suspend fun getTrackers(apps: List<ApplicationDescription>?): List<Tracker> {
return database.getTrackers(apps?.map { it.apId })
}
- fun getCallsByApps(
- periodCount: Int,
- periodUnit: TemporalUnit
- ): Map<ApplicationDescription, Pair<Int, Int>> {
- return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp()
- }
-
fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair<Int, Int> {
return database.getCalls(app.apId, periodCount, periodUnit)
}