diff --git a/app/build.gradle b/app/build.gradle index 95bbee6fa0268d9617a641740b33ca26d97a8bb5..a10ead1cc061f636449224c82c3efc399c7a5502 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 efcd096cc906a005f7664a7a688353d901f4ea9a..55183e9a40e2503741418432190226eae1043f39 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 { 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 0000000000000000000000000000000000000000..e17d69245e3f0905343956f5aa49609fcf45fb2e --- /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 . + */ +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 0000000000000000000000000000000000000000..92550abbe0371563aa6568ec67fcdaddb6dff8f8 --- /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 . + */ +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 { + 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> { + 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): List> { + 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 0000000000000000000000000000000000000000..27f3e7839b9ccbcfcffdce820436e96b125c3261 --- /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 . + */ +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> { + return enrichWithBlockedState( + statsDatabase.getApIds(tracker.id).mapNotNull { + appListsRepository.getDisplayableApp(it) + }.sortedBy { it.label?.toString() }, + tracker + ) + } + + suspend fun enrichWithBlockedState(apps: List, tracker: Tracker): List> { + return apps.map { it to !filterHostnameUseCase.isWhitelisted(it.uid, tracker.id) } + } + + suspend fun getCalls(tracker: Tracker): Pair { + 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 0000000000000000000000000000000000000000..8292a6d1d9cd85913699926faf3728cbaf2f3742 --- /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 . + */ +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> { + 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): List { + return countByTracker.map { (tracker, count) -> + TrackerWithAppsCount(tracker = tracker, appsCount = count) + }.sortedByDescending { it.appsCount } + } + + private suspend fun buildAppList(countByApp: Map): List { + return appListsRepository.apps().first().map { app: ApplicationDescription -> + AppWithTrackersCount(app = app, trackersCount = countByApp[app] ?: 0) + }.sortedByDescending { it.trackersCount } + } + + private suspend fun mapIdsToEntities(trackersAndAppsIds: List>): List> { + 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, Map> { + return trackersAndApps.fold( + mutableMapOf() to mutableMapOf() + ) { (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 2c47d709d6bb67e36935d2f48752c2de128bb62b..dddc6a2852d37685e362fe9cf2c08dc8a546237a 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 3d6ade0f212e876d53e8ce229cb85727afa93286..8f290b85437e47b41897568b0bc8271c6599f20e 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> { - 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 { + suspend fun getCalls(app: ApplicationDescription): Pair { return appListsRepository.mapReduceForHiddenApps( app = app, map = { @@ -211,67 +191,9 @@ class TrackersStatisticsUseCase( ) } - fun getAppsWithCounts(): Flow> { - 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 { return whitelistRepository.getWhiteListForApp(app).mapNotNull { trackersRepository.getTracker(it) } } - - private val mostLeakedAppsComparator: Comparator = 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 similarity index 55% rename from app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt rename to app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt index aee1890358e4e27a50d97cb45a86498b8a729d12..f00dff8f5c833fbffe2e905f2ff80b8ea5e2399f 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 . */ -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() { - 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 = emptyList() + var dataSet: List = 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 0000000000000000000000000000000000000000..242041003892acfa6376d2513a78c26e8823164e --- /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 . + */ +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() { + private var apps: List = emptyList() + private var trackers: List = emptyList() + + fun updateDataSet(apps: List?, trackers: List?) { + 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) { + (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) { + (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 0000000000000000000000000000000000000000..183a5ca93e3b2838aea3b634d883870a793ddd76 --- /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 . + */ +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 0000000000000000000000000000000000000000..3270bf32ff3e56c9c0a2655f4afc777f86526d0e --- /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 . + */ + +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() { + + 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 = 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 132fa3b445fc0e244ecc1142cf12bab1314fe88e..b016c5e21560b085a94d61d685ba6cc10485dc09 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 13719e43e55e6d1014582e9a71ffc2ce33bcff4f..7f5fdfeb2253a91307a217b05d09e9723908cddf 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? = null, + val apps: List? = null, + val trackers: List? = 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 8a5d0f0d89f9260739c354bcdca013ddc693dd55..31da8ca79a76928073c71c0c4a982f4d46a37d21 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 7fb9ca6db479b8ae2ec9824242ffbee636dd32ef..85c5350552066797366ce3b08289ca49b40ef7c8 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 a597da6462f598e31c6f1a71936af377fd757364..cea99a6e3cf7ce4c2a18ce54bfeb576a8fdd2bd2 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>? = null, + val trackersWithBlockedList: List> = emptyList(), val leaked: Int = 0, val blocked: Int = 0, val isTrackersBlockingEnabled: Boolean = false, val isWhitelistEmpty: Boolean = true, - val showQuickPrivacyDisabledMessage: Boolean = false, ) { - fun getTrackersStatus(): List>? { - 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 87407792445b339831ead312a4142b5491df2674..00ad36524486726f953641c9d121194b6351a1cf 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 ef845b676ce7d1f5a3fd69ddb01a76652fe2263d..1d499059949a0786d64b24e46c971f5ae9e782a2 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() { - - 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) { + val label = item.first.label + with(binding.title) { + if (item.first.exodusId != null) { - fun bind(item: Pair, 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> = emptyList() - fun updateDataSet(new: List>, isEnabled: Boolean) { - this.isEnabled = isEnabled + fun updateDataSet(new: List>) { 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 0000000000000000000000000000000000000000..d4196771756f23a1020513b43ba1a46bf15118c5 --- /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 . + */ + +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() { + + class ViewHolder( + private val binding: ApptrackersItemTrackerToggleBinding, + private val viewModel: TrackerDetailsViewModel, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: Pair) { + val (app, isWhiteListed) = item + binding.title.text = app.label + binding.toggle.apply { + this.isChecked = isWhiteListed + setOnClickListener { + viewModel.onToggleUnblockApp(app, isChecked) + } + } + } + } + + private var dataSet: List> = emptyList() + + fun updateDataSet(new: List>) { + 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 0000000000000000000000000000000000000000..481c809325b9e47a55d8a3f3502613b7e0d564fa --- /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 . + */ + +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 0000000000000000000000000000000000000000..9ae7412f102e19a4ed0c34c8b2ab551797067ff4 --- /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 . + */ + +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> = 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 0000000000000000000000000000000000000000..91a1f2a60a3d1f61d45181ec3b61d6c74b13d969 --- /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 . + */ + +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() + 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 0000000000000000000000000000000000000000..d9c839c160d300d57a631f38e1df8d3c9829fffb --- /dev/null +++ b/app/src/main/res/drawable/bg_stroke_rounded_12.xml @@ -0,0 +1,21 @@ + + + + + + \ 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 0000000000000000000000000000000000000000..9c20541df5530ff154b30ef2f588cd72d44af4d8 --- /dev/null +++ b/app/src/main/res/drawable/ic_shield_alert.xml @@ -0,0 +1,15 @@ + + + + + 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 0000000000000000000000000000000000000000..5ef1de5ab2c8cdd2244653d9ed2aa7113509c3fe --- /dev/null +++ b/app/src/main/res/drawable/pill_shape_tab_bg.xml @@ -0,0 +1,22 @@ + + + + + + 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 0000000000000000000000000000000000000000..344a049792c6b521fc376a2e45c5b44c6b03d4c1 --- /dev/null +++ b/app/src/main/res/drawable/pill_shape_tab_indicator.xml @@ -0,0 +1,21 @@ + + + + + + \ 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 0000000000000000000000000000000000000000..520d9851b084e57f219a82de82231e6d235b2f45 --- /dev/null +++ b/app/src/main/res/drawable/pill_shape_tab_selected.xml @@ -0,0 +1,22 @@ + + + + + + \ 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 d0a72d585f67e2afbd09b1e7b89c2b710dff56f1..06b8d3fb5bf6545c472d54c6c6ef50fdd975c121 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 . --> - - - - + android:background="@color/background" + android:layout_height="match_parent" + android:layout_width="match_parent" + > @@ -37,80 +34,137 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" > - - + android:orientation="vertical" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="32dp" + > + + + + + + + - + + + + + + + + + - - - - - - - - \ No newline at end of file + 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 db7086f314565cce755083b2440c5555ce355935..753e734003ecdc25d4a1492907c1a179bc4f7fea 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 @@ + + + + diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml index 3c709e9ca855a30e16946232020ff647f80c7d81..5da95e1214966ab5eb6bd06b02a9930c7b2cd951 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 @@ + + - - + - - - - + + + - \ No newline at end of file + 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 0000000000000000000000000000000000000000..793816546ebce8a285776de34346bd064ab49979 --- /dev/null +++ b/app/src/main/res/layout/highlight_data_number.xml @@ -0,0 +1,58 @@ + + + + + + + \ 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 0000000000000000000000000000000000000000..45ba0e49cc2349758a28c772d99755468edc903b --- /dev/null +++ b/app/src/main/res/layout/trackerdetails_fragment.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/trackers_item_app.xml b/app/src/main/res/layout/trackers_item_app.xml index 6af43ea7b04e41bc58a67ec3ed57bcc7173fb910..883a4da6057dbbaa1b8438218f0274b2e0aa6396 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 @@ + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 52a167768e10b7f07c34fb4f791e78a66868f211..1047da668f78ab6eb68be123a66f6f7865453984 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" /> + + + + + @color/e_action_bar @@ -10,14 +27,16 @@ @color/e_background @color/e_alpha_base + @color/e_disabled_color + @color/e_divider_color + + @color/e_background_overlay #263238 #FFFFFFFF - @color/e_disabled_color #28C97C #F8432E #AADCFE - @color/e_background_overlay @color/e_primary_text_color_dark diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ba3ba03e79daf32a26957323fb145442d0241355..aa33837317195d890793229d9edd2dea5f989d9a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,21 @@ + Advanced Privacy @@ -80,25 +97,55 @@ 24 hours past month past year - Manage trackers used in applications * : + @string/ipscrambling_app_list_infos + Trackers Activity Summary + Apps + Trackers + + %s trackers detected + detected in %s apps + HH:mm MMMM d - EEE MMMM yyyy - %1$d/%2$d blocked trackers, %3$d leaks + - Block trackers - Opt for the trackers you want to activate/deactivate. + %s tracking summary + Total + Detected trackers + Blocked + Trackers + Blocked leaks + %s allowed leaks + Manage tracker + Toggle on trackers control + Toggle off the trackers you want to allow: No trackers were detected yet. If new trackers are detected they will be updated here. No trackers were detected yet. All future trackers will be blocked. No trackers were detected yet. Some trackers were unblocked previously. Enable Quick Privacy to be able to activate/deactivate trackers. - %1$d blocked trackers out of %2$d detected trackers, %3$d blocked leaks and %4$d allowed leaks. App not installed. Changes will take effect when tracker blocker is on. Reset trackers + + %s tracking summary + Detected in + Different applications + Blocked leaks + %s allowed leaks + Manage tracker + Block this tracker across all apps + Toggle off the apps for which you want to allow this tracker: + + + 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. + + Do not show again Trackers control 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 f29bb8a184cb7e8a069f1ec9dff803737e8eedd9..b44e96edf097ed077022e3e2d8722aec5afb0df4 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 { + 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 mapReduceForHiddenApps( + suspend fun mapReduceForHiddenApps( app: ApplicationDescription, - map: (ApplicationDescription) -> T, - reduce: (List) -> R + map: suspend (ApplicationDescription) -> T, + reduce: suspend (List) -> 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() @@ -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(), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c945fa39b1b0c6daf53560f9a3b45635a0079bb..0bb32ba3b418402c82be42cca03b0b165092b7d0 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 15ff8137a687bc542e12c1fec53052a83d8a86cd..a80d4dc09a7f51c38b116dea42f0b2aeb07f25ce 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 { + fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair { 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() - 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 = 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> { + 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>() + var appId = "" + if (cursor.moveToNext()) { + appId = cursor.getString(COLUMN_NAME_APPID) + } + cursor.close() + db.close() + return appId + } + } + + fun getDistinctTrackerAndApp(): List> { + 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>() 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 { + suspend fun getApIds(trackerId: String): List = 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 = ArrayList() + while (cursor.moveToNext()) { + apIds.add(cursor.getString(COLUMN_NAME_APPID)) + } + cursor.close() + db.close() + + apIds + } + } + + suspend fun getCallsForApp(apId: String): Pair = 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 = 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 = 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?): List { + suspend fun getTrackerIds(appIds: List?): List = withContext(Dispatchers.IO) { + synchronized(lock) { + val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID) + var selection: String? = null + + var selectionArg: Array? = 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 = mutableListOf() + while (cursor.moveToNext()) { + trackerIds.add(cursor.getString(COLUMN_NAME_TRACKER)) + } + cursor.close() + db.close() + trackerIds + } + } + + suspend fun getTrackers(appIds: List?): List = 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 429c5e94380687c73af9d029e00a29596d5a285b..9f37a1d2e995233a34e08941e9b05afaaa611025 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 = HashSet() private var appUidsWhitelist: Set = HashSet() - private var trackersWhitelistByApp: MutableMap> = HashMap() - private var trackersWhitelistByUid: Map> = HashMap() + private var trackersWhitelist: Set = HashSet() + + private var apIdTrackersWhitelist: Map = emptyMap() + private var appUidTrackersWhitelist: Map = 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() + 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> = 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, isWhitelisted: Boolean) = withContext(Dispatchers.IO) { + val whitelist = HashSet().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().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, isWhitelisted: Boolean) = withContext( + Dispatchers.IO + ) { + setWhitelisted( + trackerIds.map { trackerId -> buildApIdTrackerKey(apId, trackerId) }, isWhitelisted + ) + } + + suspend fun setWhitelistedAppsForTracker(apIds: List, 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 { + 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 { @@ -184,12 +279,64 @@ class WhitelistRepository( } fun getWhiteListForApp(app: ApplicationDescription): List { - 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().apply { + prefs.getStringSet(KEY_APP_TRACKER_WHITELIST, HashSet())?.let { addAll(it) } + } + + val blacklist = HashSet().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 e229cab98775cab2393dd009a6f497d5057332db..e0fae43fbe656a9e833edf6e766e70667d8fdd30 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() 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 e7a84b8fa7ba3d03058b8fc7eddbb5c19a5470ac..22bd8fceb32a51c974397d81a90940bf998fe317 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 { - return database.getContactedTrackersCountByAppId().mapByAppIdToApp() - } - fun getContactedTrackersCount(): Int { return database.getContactedTrackersCount() } - fun getTrackers(apps: List?): List { + suspend fun getTrackers(apps: List?): List { return database.getTrackers(apps?.map { it.apId }) } - fun getCallsByApps( - periodCount: Int, - periodUnit: TemporalUnit - ): Map> { - return database.getCallsByAppIds(periodCount, periodUnit).mapByAppIdToApp() - } - fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair { return database.getCalls(app.apId, periodCount, periodUnit) }