From f43f65da304434b8f21b8b17cb7f80f94cff1357 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Mon, 23 Oct 2023 22:27:19 +0200 Subject: [PATCH 1/9] 1203: add list of trackers in trackers screen. --- .../usecases/TrackersStatisticsUseCase.kt | 28 ++++++++ .../trackers}/AppsAdapter.kt | 39 +++++------ .../features/trackers/TrackersAdapter.kt | 64 +++++++++++++++++++ .../features/trackers/TrackersFragment.kt | 28 ++++++-- .../features/trackers/TrackersState.kt | 17 ++++- .../features/trackers/TrackersViewModel.kt | 36 +++++++++-- app/src/main/res/layout/fragment_trackers.xml | 22 ++++++- app/src/main/res/layout/trackers_item_app.xml | 21 +++--- app/src/main/res/values/strings.xml | 9 ++- .../data/repositories/AppListsRepository.kt | 11 ++++ .../trackers/data/StatsDatabase.kt | 21 ++++++ 11 files changed, 246 insertions(+), 50 deletions(-) rename app/src/main/java/foundation/e/advancedprivacy/{common => features/trackers}/AppsAdapter.kt (55%) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt 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 3d6ade0f..26240148 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 @@ -24,6 +24,8 @@ 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.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.data.WhitelistRepository @@ -211,6 +213,32 @@ class TrackersStatisticsUseCase( ) } + // TODO move to a specific usecase and decompose in a few funcitons (?) + suspend fun getAppsAndTrackersCounts(): Pair, List> { + return statsDatabase.getDistinctTrackerAndApp().mapNotNull { (trackerId, apId) -> + trackersRepository.getTracker(trackerId)?.let { tracker -> + appListsRepository.getDisplayableApp(apId)?.let { app -> + tracker to app + } + } + }.distinct() + .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 + }.let { (countByApp, countBytracker) -> + val appList = countByApp.map { (app, count) -> + AppWithTrackersCount(app = app, trackersCount = count) + }.sortedByDescending { it.trackersCount } + + val trackerList = countBytracker.map { (tracker, count) -> + TrackerWithAppsCount(tracker = tracker, appsCount = count) + }.sortedByDescending { it.appsCount } + appList to trackerList + } + } + fun getAppsWithCounts(): Flow> { val trackersCounts = statisticsUseCase.getContactedTrackersCountByApp() val hiddenAppsTrackersWithWhiteList = 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 aee18903..f00dff8f 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/TrackersAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt new file mode 100644 index 00000000..3270bf32 --- /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 132fa3b4..18842ea3 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 @@ -37,7 +37,6 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager 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 @@ -69,11 +68,20 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { binding.apps.apply { layoutManager = LinearLayoutManager(requireContext()) setHasFixedSize(true) - adapter = AppsAdapter(R.layout.trackers_item_app) { appUid -> - viewModel.submitAction( - TrackersViewModel.Action.ClickAppAction(appUid) - ) - } + adapter = AppsAdapter(viewModel) + } + + binding.trackers.apply { + layoutManager = LinearLayoutManager(requireContext()) + setHasFixedSize(true) + adapter = TrackersAdapter(viewModel) + } + + binding.trackersToggleListApps.setOnClickListener { + viewModel.onClickShowListApps() + } + binding.trackersToggleListTrackers.setOnClickListener { + viewModel.onClickShowListTrackers() } val infoText = getString(R.string.trackers_info) @@ -164,11 +172,19 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) } state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) } + binding.apps.isVisible = state.displayApps + binding.trackers.isVisible = !state.displayApps + state.apps?.let { binding.apps.post { (binding.apps.adapter as AppsAdapter?)?.dataSet = it } } + state.trackers?.let { + binding.trackers.post { + (binding.trackers.adapter as TrackersAdapter?)?.dataSet = it + } + } } private fun renderGraph( 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 13719e43..9f1705ef 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 @@ -17,12 +17,25 @@ 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 displayApps: Boolean = true, + 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 8a5d0f0d..8fdf7da5 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,7 +22,9 @@ 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.TrackersStatisticsUseCase +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -65,9 +67,12 @@ class TrackersViewModel( ) } } - }, - trackersStatisticsUseCase.getAppsWithCounts().map { - _state.update { s -> s.copy(apps = it) } + + trackersStatisticsUseCase.getAppsAndTrackersCounts().let { (appList, trackerList) -> + _state.update { + it.copy(apps = appList, trackers = trackerList) + } + } } ).collect {} } @@ -80,9 +85,30 @@ class TrackersViewModel( } } + fun onClickShowListApps() { + _state.update { + it.copy(displayApps = true) + } + } + + fun onClickShowListTrackers() { + _state.update { + it.copy(displayApps = false) + } + } + + fun onClickTracker(tracker: Tracker) { + // TODO !! + tracker.id + } + + fun onClickApp(app: ApplicationDescription) = viewModelScope.launch { + _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = app.uid)) + } + private suspend fun actionClickApp(action: Action.ClickAppAction) { - state.value.apps?.find { it.uid == action.appUid }?.let { - _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = it.uid)) + state.value.apps?.find { it.app.uid == action.appUid }?.let { + _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = it.app.uid)) } } diff --git a/app/src/main/res/layout/fragment_trackers.xml b/app/src/main/res/layout/fragment_trackers.xml index 0cd5980b..6503994b 100644 --- a/app/src/main/res/layout/fragment_trackers.xml +++ b/app/src/main/res/layout/fragment_trackers.xml @@ -74,6 +74,19 @@ android:paddingHorizontal="16dp" android:text="@string/trackers_applist_title" /> +