From 680b7a48553d1f1a785d8cf542f2ce83afde56ba Mon Sep 17 00:00:00 2001 From: jacquarg Date: Sun, 10 Mar 2024 20:38:43 +0100 Subject: [PATCH 1/4] feat:1960: trackers and apps Wall of shame --- .../e/advancedprivacy/KoinModule.kt | 5 +- .../repositories/LocalStateRepositoryImpl.kt | 4 +- .../usecases/TrackersAndAppsListsUseCase.kt | 50 ++++++- .../domain/usecases/TrackersScreenUseCase.kt | 15 ++- .../usecases/TrackersStatisticsUseCase.kt | 6 +- .../features/dashboard/DashboardFragment.kt | 19 +++ .../features/dashboard/DashboardState.kt | 7 +- .../features/dashboard/DashboardViewModel.kt | 36 ++++- .../dashboard/ShameListsTabPagerAdapter.kt | 127 ++++++++++++++++++ .../features/trackers/AppsAdapter.kt | 11 +- .../features/trackers/ListsTabPagerAdapter.kt | 6 +- .../features/trackers/TrackersAdapter.kt | 11 +- .../features/trackers/TrackersFragment.kt | 6 +- .../trackers/TrackersPeriodFragment.kt | 9 +- .../trackers/TrackersPeriodViewModel.kt | 11 +- .../features/trackers/TrackersState.kt | 7 +- .../features/trackers/TrackersViewModel.kt | 3 +- .../e/advancedprivacy/widget/Widget.kt | 2 +- .../main/res/layout/dashboard_shame_list.xml | 46 +++++++ .../main/res/layout/fragment_dashboard.xml | 47 ++++++- app/src/main/res/navigation/nav_graph.xml | 5 +- app/src/main/res/values/strings.xml | 6 +- .../repositories/LocalStateRepository.kt | 4 +- .../trackers/data/StatsDatabase.kt | 54 +++++++- 24 files changed, 451 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt create mode 100644 app/src/main/res/layout/dashboard_shame_list.xml diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 23065911..c11260e7 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 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 @@ -185,7 +185,8 @@ val appModule = module { TrackersPeriodViewModel( period = period, trackersStatisticsUseCase = get(), - trackersAndAppsListsUseCase = get() + trackersAndAppsListsUseCase = get(), + trackersScreenUseCase = get() ) } diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt index c131eef5..4bd5f203 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023-2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -130,6 +130,8 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { get() = sharedPref.getInt(KEY_TRACKERS_SCREEN_LAST_POSITION, 0) set(value) = sharedPref.edit().putInt(KEY_TRACKERS_SCREEN_LAST_POSITION, value).apply() + override var trackersScreenTabStartPosition: Int = 0 + private fun set(key: String, value: Boolean) { sharedPref.edit().putBoolean(key, value).apply() } 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 index 5606d499..84323c79 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023-2024 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 @@ -34,10 +34,7 @@ class TrackersAndAppsListsUseCase( private val appListsRepository: AppListsRepository ) { suspend fun getAppsAndTrackersCounts(period: Period): TrackersAndAppsLists { - val periodStart: Instant = period.getPeriodStart() - val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart) - val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) - val (countByApp, countByTracker) = foldToCountByEntityMaps(trackersAndApps) + val (countByApp, countByTracker) = getCountByEntityMaps(period) return TrackersAndAppsLists( trackers = buildTrackerList(countByTracker), @@ -46,6 +43,49 @@ class TrackersAndAppsListsUseCase( ) } + suspend fun getWallOfShame(): TrackersAndAppsLists { + val trackers = statsDatabase + .get5MostCalledTrackers(since = Period.MONTH.getPeriodStart().epochSecond) + .mapNotNull { trackerId -> + trackersRepository.getTracker(trackerId) + } + + val apps = get5MostTrackedAppsLastMonth() + + val (countByApp, countByTracker) = getCountByEntityMaps(Period.MONTH) + + val trackerWithAppsCounts = trackers.mapNotNull { tracker -> + TrackerWithAppsCount(tracker, countByTracker[tracker]!!) + } + val appsWithTrackersCounts = apps.map { app -> + AppWithTrackersCount(app, countByApp[app]!!) + } + return TrackersAndAppsLists( + trackers = trackerWithAppsCounts, + appsWithTrackers = appsWithTrackersCounts, + allApps = emptyList() + ) + } + + private suspend fun get5MostTrackedAppsLastMonth(): List { + val countByAppIds = statsDatabase.getCallsByAppIds(since = Period.MONTH.getPeriodStart().epochSecond) + + val countByApps = mutableMapOf() + countByAppIds.forEach { (appId, count) -> + appListsRepository.getDisplayableApp(appId)?.let { app -> + countByApps[app] = count + (countByApps[app] ?: 0) + } + } + return countByApps.toList().sortedByDescending { it.second }.take(5).map { it.first } + } + + private suspend fun getCountByEntityMaps(period: Period): Pair, Map> { + val periodStart: Instant = period.getPeriodStart() + val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart) + val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) + return foldToCountByEntityMaps(trackersAndApps) + } + private fun buildTrackerList(countByTracker: Map): List { return countByTracker.map { (tracker, count) -> TrackerWithAppsCount(tracker = tracker, appsCount = count) diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt index 16941443..eaa4cb7a 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 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 @@ -30,4 +30,17 @@ class TrackersScreenUseCase(private val localStateRepository: LocalStateReposito suspend fun savePosition(currentPosition: Int) = withContext(Dispatchers.IO) { localStateRepository.trackersScreenLastPosition = currentPosition } + + fun getTrackerTabStartPosition(): Int { + return localStateRepository.trackersScreenTabStartPosition + } + + fun resetTrackerTabStartPosition() { + localStateRepository.trackersScreenTabStartPosition = 0 + } + + suspend fun preselectTab(periodPosition: Int, tabPosition: Int) = withContext(Dispatchers.IO) { + localStateRepository.trackersScreenLastPosition = periodPosition + localStateRepository.trackersScreenTabStartPosition = tabPosition + } } 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 765255fc..0b50c8e1 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,5 @@ /* - * Copyright (C) 2022 - 2023 MURENA SAS + * Copyright (C) 2022 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -177,8 +177,8 @@ class TrackersStatisticsUseCase( return statsDatabase.getBlockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit) } - suspend fun getLastMonthAppsWithBLockedLeaksCount(): Int { - return statsDatabase.getAppsWithBLockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit) + suspend fun getLastMonthAppsWithBlockedLeaksCount(): Int { + return statsDatabase.getAppsWithBlockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit) } private fun getWhiteList(app: ApplicationDescription): List { diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt index 4b98d6d0..8e2e5811 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import com.google.android.material.tabs.TabLayoutMediator import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.BigNumberFormatter import foundation.e.advancedprivacy.common.NavToolbarFragment @@ -35,6 +36,7 @@ import foundation.e.advancedprivacy.databinding.FragmentDashboardBinding import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.TrackerMode import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel.SingleEvent +import foundation.e.advancedprivacy.features.trackers.TrackerTab import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel @@ -44,6 +46,8 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { private lateinit var binding: FragmentDashboardBinding + private lateinit var tabAdapter: ShameListsTabPagerAdapter + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentDashboardBinding.bind(view) @@ -79,6 +83,19 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { binding.fakeLocation.title.setText(R.string.dashboard_location_title) binding.ipScrambling.title.setText(R.string.dashboard_ipscrambling_title) + tabAdapter = ShameListsTabPagerAdapter(viewModel) + + binding.listsPager.adapter = tabAdapter + + TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position -> + tab.text = getString( + when (position) { + TrackerTab.APPS.position -> R.string.trackers_toggle_list_apps + else -> R.string.trackers_toggle_list_trackers + } + ) + }.attach() + setOnClickListeners() listenViewModel() @@ -213,6 +230,8 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { ) stateLabel.setTextColor(getStateColor(state.ipScramblingMode == FeatureState.ON)) } + + tabAdapter.updateDataSet(state) } private fun getStateColor(isActive: Boolean): Int { diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt index 7dc3c0b5..2941dd89 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2024 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -19,11 +20,15 @@ package foundation.e.advancedprivacy.features.dashboard import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.TrackerMode +import foundation.e.advancedprivacy.features.trackers.AppWithTrackersCount +import foundation.e.advancedprivacy.features.trackers.TrackerWithAppsCount data class DashboardState( val trackerMode: TrackerMode = TrackerMode.VULNERABLE, val isLocationHidden: Boolean = false, val ipScramblingMode: FeatureState = FeatureState.STOPPING, val blockedCallsCount: Int = 0, - val appsWithCallsCount: Int = 0 + val appsWithCallsCount: Int = 0, + val shameApps: List = emptyList(), + val shameTrackers: List = emptyList() ) diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt index a820be65..1e019332 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -23,8 +23,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase +import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase +import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase +import foundation.e.advancedprivacy.features.trackers.Period +import foundation.e.advancedprivacy.features.trackers.TrackerTab +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -39,7 +45,9 @@ import kotlinx.coroutines.withContext class DashboardViewModel( private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val trackersStatisticsUseCase: TrackersStatisticsUseCase + private val trackersStatisticsUseCase: TrackersStatisticsUseCase, + private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase, + private val trackersScreenUseCase: TrackersScreenUseCase ) : ViewModel() { private val _state = MutableStateFlow(DashboardState()) @@ -113,15 +121,35 @@ class DashboardViewModel( _navigate.emit(DashboardFragmentDirections.gotoSettingsPermissionsActivity()) } + fun onClickShameApp(app: ApplicationDescription) = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoAppTrackersFragment(appUid = app.uid)) + } + + fun onClickShameTracker(tracker: Tracker) = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoTrackerDetailsFragment(trackerId = tracker.id)) + } + + fun onClickViewAllApps() = viewModelScope.launch { + trackersScreenUseCase.preselectTab(Period.MONTH.ordinal, TrackerTab.APPS.ordinal) + _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) + } + + fun onClickViewAllTrackers() = viewModelScope.launch { + trackersScreenUseCase.preselectTab(Period.MONTH.ordinal, TrackerTab.TRACKERS.ordinal) + _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) + } private suspend fun fetchStatistics() = withContext(Dispatchers.IO) { val blockedCallsCount = trackersStatisticsUseCase.getLastMonthBlockedLeaksCount() - val appsWithBlockedLeaksCount = trackersStatisticsUseCase.getLastMonthAppsWithBLockedLeaksCount() + val appsWithBlockedLeaksCount = trackersStatisticsUseCase.getLastMonthAppsWithBlockedLeaksCount() + val lists = trackersAndAppsListsUseCase.getWallOfShame() _state.update { it.copy( blockedCallsCount = blockedCallsCount, - appsWithCallsCount = appsWithBlockedLeaksCount + appsWithCallsCount = appsWithBlockedLeaksCount, + shameApps = lists.appsWithTrackers, + shameTrackers = lists.trackers ) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt new file mode 100644 index 00000000..96114783 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2023 - 2024 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.dashboard + +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.DashboardShameListBinding +import foundation.e.advancedprivacy.features.trackers.AppsAdapter +import foundation.e.advancedprivacy.features.trackers.TrackerTab +import foundation.e.advancedprivacy.features.trackers.TrackersAdapter + +class ShameListsTabPagerAdapter( + private val viewModel: DashboardViewModel +) : RecyclerView.Adapter() { + private var uiState: DashboardState = DashboardState() + + fun updateDataSet(state: DashboardState) { + uiState = state + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int = position + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListsTabViewHolder { + val view = DashboardShameListBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return when (viewType) { + TrackerTab.APPS.position -> { + ListsTabViewHolder.AppsListViewHolder(view, viewModel) + } + else -> { + ListsTabViewHolder.TrackersListViewHolder(view, viewModel) + } + } + } + + override fun getItemCount(): Int { + return 2 + } + + override fun onBindViewHolder(holder: ListsTabViewHolder, position: Int) { + when (position) { + TrackerTab.APPS.position -> { + (holder as ListsTabViewHolder.AppsListViewHolder).onBind(uiState) + } + TrackerTab.TRACKERS.position -> { + (holder as ListsTabViewHolder.TrackersListViewHolder).onBind(uiState) + } + } + } + + 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: DashboardShameListBinding, + private val viewModel: DashboardViewModel + ) : ListsTabViewHolder(binding.root) { + init { + setupRecyclerView(binding.list) + binding.list.adapter = AppsAdapter(viewModel::onClickShameApp) + binding.viewAll.apply { + text = binding.root.context.getString(R.string.dashboard_view_all_apps) + setOnClickListener { viewModel.onClickViewAllApps() } + } + } + + fun onBind(uiState: DashboardState) { + (binding.list.adapter as AppsAdapter).dataSet = uiState.shameApps + } + } + + class TrackersListViewHolder( + private val binding: DashboardShameListBinding, + private val viewModel: DashboardViewModel + ) : ListsTabViewHolder(binding.root) { + init { + setupRecyclerView(binding.list) + binding.list.adapter = TrackersAdapter(viewModel::onClickShameTracker) + binding.viewAll.apply { + text = binding.root.context.getString(R.string.dashboard_view_all_trackers) + setOnClickListener { viewModel.onClickViewAllTrackers() } + } + } + + fun onBind(uiState: DashboardState) { + (binding.list.adapter as TrackersAdapter).dataSet = uiState.shameTrackers + } + } + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt index dcfd8176..12b762d7 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 - 2023 MURENA SAS + * Copyright (C) 2022 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -24,20 +24,21 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription class AppsAdapter( - private val viewModel: TrackersPeriodViewModel + private val onClickApp: (ApplicationDescription) -> Unit ) : RecyclerView.Adapter() { - class ViewHolder(view: View, private val parentViewModel: TrackersPeriodViewModel) : RecyclerView.ViewHolder(view) { + class ViewHolder(view: View, private val onClickApp: (ApplicationDescription) -> Unit) : 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) + onClickApp(item.app) } } } @@ -51,7 +52,7 @@ class AppsAdapter( 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) + return ViewHolder(view, onClickApp) } 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 index 566a9cb8..ebe42e10 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 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 @@ -104,7 +104,7 @@ class ListsTabPagerAdapter( ) : ListsTabViewHolder(binding.root) { init { setupRecyclerView(binding.list) - binding.list.adapter = AppsAdapter(viewModel) + binding.list.adapter = AppsAdapter(viewModel::onClickApp) binding.toggleNoTrackerApps.setOnClickListener { viewModel.onToggleHideNoTrackersApps() } } @@ -130,7 +130,7 @@ class ListsTabPagerAdapter( ) : ListsTabViewHolder(binding.root) { init { setupRecyclerView(binding.list) - binding.list.adapter = TrackersAdapter(viewModel) + binding.list.adapter = TrackersAdapter(viewModel::onClickTracker) } fun onBind(trackers: List) { 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 index 135d43e0..53b11b43 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 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 @@ -24,13 +24,14 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker class TrackersAdapter( - val viewModel: TrackersPeriodViewModel + private val onClickTracker: (Tracker) -> Unit ) : RecyclerView.Adapter() { - class ViewHolder(view: View, private val parentViewModel: TrackersPeriodViewModel) : RecyclerView.ViewHolder(view) { + class ViewHolder(view: View, private val onClickTracker: (Tracker) -> Unit) : RecyclerView.ViewHolder(view) { val binding = TrackersItemAppBinding.bind(view) init { binding.icon.isVisible = false @@ -39,7 +40,7 @@ class TrackersAdapter( 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) + onClickTracker(item.tracker) } } } @@ -52,7 +53,7 @@ class TrackersAdapter( 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) + return ViewHolder(view, onClickTracker) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { 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 6518de1f..5f55b1a8 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 MURENA SAS + * Copyright (C) 2022 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -79,8 +79,8 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { listenViewModel() } - override fun onResume() { - super.onResume() + override fun onStart() { + super.onStart() lifecycleScope.launch { binding.trackersPeriodsPager.currentItem = viewModel.getLastPosition() } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt index f051407d..5f3acaf1 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 MURENA SAS + * Copyright (C) 2022 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -103,6 +103,13 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { listenViewModel() } + override fun onResume() { + super.onResume() + lifecycleScope.launch { + binding.listsPager.currentItem = viewModel.getStartPosition() + } + } + @OptIn(FlowPreview::class) private fun listenViewModel() { with(viewLifecycleOwner) { diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt index b708b0b0..61e47f99 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 MURENA SAS + * Copyright (C) 2022 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -25,6 +25,7 @@ import androidx.navigation.NavDirections import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase +import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import kotlinx.coroutines.Dispatchers @@ -39,7 +40,8 @@ import kotlinx.coroutines.withContext class TrackersPeriodViewModel( private val period: Period, private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase + private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase, + private val trackersScreenUseCase: TrackersScreenUseCase ) : ViewModel() { private val _state = MutableStateFlow( @@ -107,6 +109,11 @@ class TrackersPeriodViewModel( _refreshUiHeight.emit(Unit) } + fun getStartPosition(): Int { + val startPosition = trackersScreenUseCase.getTrackerTabStartPosition() + return startPosition + } + sealed class SingleEvent { data class ErrorEvent(val error: String) : SingleEvent() data class OpenUrl(val url: Uri) : SingleEvent() 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 f0787fd8..9a1b8254 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,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -57,6 +57,11 @@ data class TrackerWithAppsCount( val appsCount: Int = 0 ) +enum class TrackerTab(val position: Int) { + APPS(0), + TRACKERS(1) +} + enum class Period(val periodsCount: Int, val periodUnit: TemporalUnit) { DAY(24, ChronoUnit.HOURS), MONTH(30, ChronoUnit.DAYS), 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 945789e4..b682adfb 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022-2023 MURENA SAS + * Copyright (C) 2022 - 2024 MURENA SAS * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -68,6 +68,7 @@ class TrackersViewModel(private val trackersScreenUseCase: TrackersScreenUseCase fun onDisplayedItemChanged(position: Int) = viewModelScope.launch(Dispatchers.IO) { trackersScreenUseCase.savePosition(position) + trackersScreenUseCase.resetTrackerTabStartPosition() _refreshUiHeight.emit(Unit) } diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt index a7cec14a..cc989ff6 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/widget/Widget.kt @@ -106,7 +106,7 @@ class Widget : AppWidgetProvider() { ) { state, _ -> state.copy( blockedCallsCount = trackersStatisticsUseCase.getLastMonthBlockedLeaksCount(), - appsWithCallsCount = trackersStatisticsUseCase.getLastMonthAppsWithBLockedLeaksCount() + appsWithCallsCount = trackersStatisticsUseCase.getLastMonthAppsWithBlockedLeaksCount() ) }.stateIn( scope = coroutineScope, diff --git a/app/src/main/res/layout/dashboard_shame_list.xml b/app/src/main/res/layout/dashboard_shame_list.xml new file mode 100644 index 00000000..51c2814f --- /dev/null +++ b/app/src/main/res/layout/dashboard_shame_list.xml @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index fd685c92..73db7348 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -1,4 +1,19 @@ - + + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 83636df8..ea5bb250 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -1,6 +1,6 @@ Manage my Internet address diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt index 86e1acf9..39212a23 100644 --- a/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 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 @@ -58,4 +58,6 @@ interface LocalStateRepository { var hideWarningIpScrambling: Boolean var trackersScreenLastPosition: Int + + var trackersScreenTabStartPosition: Int } 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 beaf109b..c3a6fe8b 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2023 - 2024 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -350,7 +350,7 @@ class StatsDatabase( } } - suspend fun getAppsWithBLockedLeaksCount(periodCount: Int, periodUnit: TemporalUnit): Int = withContext(Dispatchers.IO) { + suspend fun getAppsWithBlockedLeaksCount(periodCount: Int, periodUnit: TemporalUnit): Int = withContext(Dispatchers.IO) { synchronized(lock) { val minTimestamp = getPeriodStartTs(periodCount, periodUnit) val db = readableDatabase @@ -371,6 +371,56 @@ class StatsDatabase( } } + suspend fun getCallsByAppIds(since: Long): Map = withContext(Dispatchers.IO) { + synchronized(lock) { + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + since) + val projection = "$COLUMN_NAME_APPID, " + + "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM" + val cursor = db.rawQuery( + "SELECT $projection FROM $TABLE_NAME" + + " WHERE $selection" + + " GROUP BY $COLUMN_NAME_APPID", + selectionArg + ) + val callsByApp = HashMap() + + while (cursor.moveToNext()) { + val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) + callsByApp[cursor.getString(COLUMN_NAME_APPID)] = contacted + } + cursor.close() + db.close() + callsByApp + } + } + + suspend fun get5MostCalledTrackers(since: Long): List = withContext(Dispatchers.IO) { + synchronized(lock) { + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + since) + val projection = "$COLUMN_NAME_TRACKER, " + + "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM" + val cursor = db.rawQuery( + "SELECT $projection FROM $TABLE_NAME" + + " WHERE $selection" + + " GROUP BY $COLUMN_NAME_TRACKER" + + " ORDER BY $PROJECTION_NAME_CONTACTED_SUM DESC" + + " LIMIT 5", + selectionArg + ) + val trackers = mutableListOf() + while (cursor.moveToNext()) { + trackers.add(cursor.getString(COLUMN_NAME_TRACKER)) + } + cursor.close() + db.close() + trackers + } + } + suspend fun logAccess(trackerId: String?, appId: String, blocked: Boolean) = withContext( Dispatchers.IO ) { -- GitLab From 790938f16ef1cd7aba66ed32405154084c132db8 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Sun, 7 Apr 2024 21:41:04 +0200 Subject: [PATCH 2/4] feat:1960: Updates labels of wall of shame. --- .../advancedprivacy/common/BindingAdapter.kt | 16 ++++ .../domain/entities/TrackersAndAppsLists.kt | 19 +++-- .../usecases/TrackersAndAppsListsUseCase.kt | 50 ++++++------- .../domain/usecases/TrackersScreenUseCase.kt | 2 +- .../features/dashboard/DashboardState.kt | 8 +- .../features/dashboard/DashboardViewModel.kt | 2 +- .../dashboard/ShameListsTabPagerAdapter.kt | 75 +++++++++++++++++-- .../features/trackers/AppsAdapter.kt | 64 ---------------- .../features/trackers/ListsTabPagerAdapter.kt | 65 ++++++++++++++-- .../features/trackers/TrackersAdapter.kt | 65 ---------------- .../trackers/TrackersPeriodFragment.kt | 4 +- .../trackers/TrackersPeriodViewModel.kt | 4 +- .../features/trackers/TrackersState.kt | 20 ++--- .../layout/dashboard_item_submenu_button.xml | 1 + .../main/res/layout/fragment_dashboard.xml | 12 +-- app/src/main/res/values/strings.xml | 2 + .../trackers/data/StatsDatabase.kt | 8 +- 17 files changed, 209 insertions(+), 208 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt delete mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt delete mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt new file mode 100644 index 00000000..62944c8b --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt @@ -0,0 +1,16 @@ +package foundation.e.advancedprivacy.common + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class BindingViewHolder(val binding: T) : RecyclerView.ViewHolder(binding.root) + +abstract class BindingListAdapter : RecyclerView.Adapter>() { + var dataSet: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun getItemCount(): Int = dataSet.size +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersAndAppsLists.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersAndAppsLists.kt index 2113b3a3..e8444732 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersAndAppsLists.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersAndAppsLists.kt @@ -16,11 +16,20 @@ */ package foundation.e.advancedprivacy.domain.entities -import foundation.e.advancedprivacy.features.trackers.AppWithTrackersCount -import foundation.e.advancedprivacy.features.trackers.TrackerWithAppsCount +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker data class TrackersAndAppsLists( - val trackers: List, - val allApps: List, - val appsWithTrackers: List + val trackers: List, + val allApps: List, + val appsWithTrackers: List +) + +data class AppWithCount( + val app: ApplicationDescription, + val count: Int = 0 +) + +data class TrackerWithCount( + val tracker: Tracker, + val count: Int = 0 ) 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 index 84323c79..62e37ae2 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt @@ -17,11 +17,11 @@ package foundation.e.advancedprivacy.domain.usecases import foundation.e.advancedprivacy.data.repositories.AppListsRepository +import foundation.e.advancedprivacy.domain.entities.AppWithCount import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.TrackerWithCount import foundation.e.advancedprivacy.domain.entities.TrackersAndAppsLists -import foundation.e.advancedprivacy.features.trackers.AppWithTrackersCount import foundation.e.advancedprivacy.features.trackers.Period -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 @@ -43,31 +43,23 @@ class TrackersAndAppsListsUseCase( ) } - suspend fun getWallOfShame(): TrackersAndAppsLists { + suspend fun buildWallOfShame(): TrackersAndAppsLists { val trackers = statsDatabase .get5MostCalledTrackers(since = Period.MONTH.getPeriodStart().epochSecond) - .mapNotNull { trackerId -> - trackersRepository.getTracker(trackerId) + .mapNotNull { (trackerId, calls) -> + trackersRepository.getTracker(trackerId)?.let { + TrackerWithCount(it, calls) + } } - val apps = get5MostTrackedAppsLastMonth() - - val (countByApp, countByTracker) = getCountByEntityMaps(Period.MONTH) - - val trackerWithAppsCounts = trackers.mapNotNull { tracker -> - TrackerWithAppsCount(tracker, countByTracker[tracker]!!) - } - val appsWithTrackersCounts = apps.map { app -> - AppWithTrackersCount(app, countByApp[app]!!) - } return TrackersAndAppsLists( - trackers = trackerWithAppsCounts, - appsWithTrackers = appsWithTrackersCounts, + trackers = trackers, + appsWithTrackers = get5MostTrackedAppsLastMonth(), allApps = emptyList() ) } - private suspend fun get5MostTrackedAppsLastMonth(): List { + private suspend fun get5MostTrackedAppsLastMonth(): List { val countByAppIds = statsDatabase.getCallsByAppIds(since = Period.MONTH.getPeriodStart().epochSecond) val countByApps = mutableMapOf() @@ -76,7 +68,9 @@ class TrackersAndAppsListsUseCase( countByApps[app] = count + (countByApps[app] ?: 0) } } - return countByApps.toList().sortedByDescending { it.second }.take(5).map { it.first } + return countByApps.toList().sortedByDescending { it.second }.take(5).map { (app, count) -> + AppWithCount(app, count) + } } private suspend fun getCountByEntityMaps(period: Period): Pair, Map> { @@ -86,22 +80,22 @@ class TrackersAndAppsListsUseCase( return foldToCountByEntityMaps(trackersAndApps) } - private fun buildTrackerList(countByTracker: Map): List { + private fun buildTrackerList(countByTracker: Map): List { return countByTracker.map { (tracker, count) -> - TrackerWithAppsCount(tracker = tracker, appsCount = count) - }.sortedByDescending { it.appsCount } + TrackerWithCount(tracker = tracker, count = count) + }.sortedByDescending { it.count } } - private suspend fun buildAllAppList(countByApp: Map): List { + private suspend fun buildAllAppList(countByApp: Map): List { return appListsRepository.apps().first().map { app: ApplicationDescription -> - AppWithTrackersCount(app = app, trackersCount = countByApp[app] ?: 0) - }.sortedByDescending { it.trackersCount } + AppWithCount(app = app, count = countByApp[app] ?: 0) + }.sortedByDescending { it.count } } - private fun buildAppList(countByApp: Map): List { + private fun buildAppList(countByApp: Map): List { return countByApp.map { (app, count) -> - AppWithTrackersCount(app = app, trackersCount = count) - }.sortedByDescending { it.trackersCount } + AppWithCount(app = app, count = count) + }.sortedByDescending { it.count } } private suspend fun mapIdsToEntities(trackersAndAppsIds: List>): List> { diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt index eaa4cb7a..93e398da 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt @@ -36,7 +36,7 @@ class TrackersScreenUseCase(private val localStateRepository: LocalStateReposito } fun resetTrackerTabStartPosition() { - localStateRepository.trackersScreenTabStartPosition = 0 + localStateRepository.trackersScreenTabStartPosition = -1 } suspend fun preselectTab(periodPosition: Int, tabPosition: Int) = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt index 2941dd89..e53c8879 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt @@ -18,10 +18,10 @@ package foundation.e.advancedprivacy.features.dashboard +import foundation.e.advancedprivacy.domain.entities.AppWithCount import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.TrackerMode -import foundation.e.advancedprivacy.features.trackers.AppWithTrackersCount -import foundation.e.advancedprivacy.features.trackers.TrackerWithAppsCount +import foundation.e.advancedprivacy.domain.entities.TrackerWithCount data class DashboardState( val trackerMode: TrackerMode = TrackerMode.VULNERABLE, @@ -29,6 +29,6 @@ data class DashboardState( val ipScramblingMode: FeatureState = FeatureState.STOPPING, val blockedCallsCount: Int = 0, val appsWithCallsCount: Int = 0, - val shameApps: List = emptyList(), - val shameTrackers: List = emptyList() + val shameApps: List = emptyList(), + val shameTrackers: List = emptyList() ) diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt index 1e019332..badf0ab9 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt @@ -143,7 +143,7 @@ class DashboardViewModel( val appsWithBlockedLeaksCount = trackersStatisticsUseCase.getLastMonthAppsWithBlockedLeaksCount() - val lists = trackersAndAppsListsUseCase.getWallOfShame() + val lists = trackersAndAppsListsUseCase.buildWallOfShame() _state.update { it.copy( blockedCallsCount = blockedCallsCount, diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt index 96114783..880a0297 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt @@ -21,14 +21,19 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible 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.common.BigNumberFormatter +import foundation.e.advancedprivacy.common.BindingListAdapter +import foundation.e.advancedprivacy.common.BindingViewHolder import foundation.e.advancedprivacy.databinding.DashboardShameListBinding -import foundation.e.advancedprivacy.features.trackers.AppsAdapter +import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding +import foundation.e.advancedprivacy.domain.entities.AppWithCount +import foundation.e.advancedprivacy.domain.entities.TrackerWithCount import foundation.e.advancedprivacy.features.trackers.TrackerTab -import foundation.e.advancedprivacy.features.trackers.TrackersAdapter class ShameListsTabPagerAdapter( private val viewModel: DashboardViewModel @@ -70,15 +75,18 @@ class ShameListsTabPagerAdapter( } sealed class ListsTabViewHolder(view: View) : RecyclerView.ViewHolder(view) { + protected val numberFormatter: BigNumberFormatter by lazy { BigNumberFormatter(itemView.context) } + protected fun setupRecyclerView(recyclerView: RecyclerView) { recyclerView.apply { layoutManager = LinearLayoutManager(context) setHasFixedSize(true) + isNestedScrollingEnabled = false addItemDecoration( MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply { dividerColor = ContextCompat.getColor(context, R.color.divider) -// dividerInsetStart = 16.dpToPx() -// dividerInsetEnd = 16.dpToPx() + dividerInsetStart = 16.dpToPx() + dividerInsetEnd = 16.dpToPx() } ) } @@ -92,9 +100,34 @@ class ShameListsTabPagerAdapter( private val binding: DashboardShameListBinding, private val viewModel: DashboardViewModel ) : ListsTabViewHolder(binding.root) { + + private val adapter = object : BindingListAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + return BindingViewHolder( + TrackersItemAppBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + val item = dataSet[position] + holder.binding.icon.setImageDrawable(item.app.icon) + holder.binding.title.text = item.app.label + holder.binding.counts.text = itemView.context.getString( + R.string.dashboard_wall_of_shame_app_calls, + numberFormatter.format(item.count) + ) + holder.binding.root.setOnClickListener { + viewModel.onClickShameApp(item.app) + } + } + } init { setupRecyclerView(binding.list) - binding.list.adapter = AppsAdapter(viewModel::onClickShameApp) + binding.list.adapter = adapter binding.viewAll.apply { text = binding.root.context.getString(R.string.dashboard_view_all_apps) setOnClickListener { viewModel.onClickViewAllApps() } @@ -102,7 +135,7 @@ class ShameListsTabPagerAdapter( } fun onBind(uiState: DashboardState) { - (binding.list.adapter as AppsAdapter).dataSet = uiState.shameApps + adapter.dataSet = uiState.shameApps } } @@ -110,9 +143,35 @@ class ShameListsTabPagerAdapter( private val binding: DashboardShameListBinding, private val viewModel: DashboardViewModel ) : ListsTabViewHolder(binding.root) { + + private val adapter = object : BindingListAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + return BindingViewHolder( + TrackersItemAppBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + val item = dataSet[position] + holder.binding.icon.isVisible = false + holder.binding.title.text = item.tracker.label + holder.binding.counts.text = itemView.context.getString( + R.string.dashboard_wall_of_shame_trackers_calls, + numberFormatter.format(item.count) + ) + holder.binding.root.setOnClickListener { + viewModel.onClickShameTracker(item.tracker) + } + } + } + init { setupRecyclerView(binding.list) - binding.list.adapter = TrackersAdapter(viewModel::onClickShameTracker) + binding.list.adapter = adapter binding.viewAll.apply { text = binding.root.context.getString(R.string.dashboard_view_all_trackers) setOnClickListener { viewModel.onClickViewAllTrackers() } @@ -120,7 +179,7 @@ class ShameListsTabPagerAdapter( } fun onBind(uiState: DashboardState) { - (binding.list.adapter as TrackersAdapter).dataSet = uiState.shameTrackers + adapter.dataSet = uiState.shameTrackers } } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt deleted file mode 100644 index 12b762d7..00000000 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2022 - 2024 MURENA SAS - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.advancedprivacy.features.trackers - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import foundation.e.advancedprivacy.R -import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription - -class AppsAdapter( - private val onClickApp: (ApplicationDescription) -> Unit -) : - RecyclerView.Adapter() { - - class ViewHolder(view: View, private val onClickApp: (ApplicationDescription) -> Unit) : 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 { - onClickApp(item.app) - } - } - } - - 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, onClickApp) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val app = dataSet[position] - holder.bind(app) - } - - override fun getItemCount(): Int = dataSet.size -} diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt index ebe42e10..10aeae75 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt @@ -22,12 +22,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible 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.common.BindingListAdapter +import foundation.e.advancedprivacy.common.BindingViewHolder import foundation.e.advancedprivacy.databinding.TrackersAppsListBinding +import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding import foundation.e.advancedprivacy.databinding.TrackersListBinding +import foundation.e.advancedprivacy.domain.entities.AppWithCount +import foundation.e.advancedprivacy.domain.entities.TrackerWithCount const val TAB_APPS = 0 private const val TAB_TRACKERS = 1 @@ -102,14 +108,38 @@ class ListsTabPagerAdapter( private val binding: TrackersAppsListBinding, private val viewModel: TrackersPeriodViewModel ) : ListsTabViewHolder(binding.root) { + private val adapter = object : BindingListAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + return BindingViewHolder( + TrackersItemAppBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + val item = dataSet[position] + holder.binding.icon.setImageDrawable(item.app.icon) + holder.binding.title.text = item.app.label + holder.binding.counts.text = itemView.context.getString( + R.string.trackers_list_app_trackers_counts, item.count.toString() + ) + holder.binding.root.setOnClickListener { + viewModel.onClickApp(item.app) + } + } + } + init { setupRecyclerView(binding.list) - binding.list.adapter = AppsAdapter(viewModel::onClickApp) + binding.list.adapter = adapter binding.toggleNoTrackerApps.setOnClickListener { viewModel.onToggleHideNoTrackersApps() } } fun onBind(uiState: TrackersPeriodState) { - (binding.list.adapter as AppsAdapter).dataSet = ( + adapter.dataSet = ( if (uiState.hideNoTrackersApps) { uiState.appsWithTrackers } else { @@ -128,13 +158,38 @@ class ListsTabPagerAdapter( private val binding: TrackersListBinding, private val viewModel: TrackersPeriodViewModel ) : ListsTabViewHolder(binding.root) { + + private val adapter = object : BindingListAdapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + return BindingViewHolder( + TrackersItemAppBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + val item = dataSet[position] + holder.binding.icon.isVisible = false + holder.binding.title.text = item.tracker.label + holder.binding.counts.text = itemView.context.getString( + R.string.trackers_list_tracker_apps_counts, item.count.toString() + ) + holder.binding.root.setOnClickListener { + viewModel.onClickTracker(item.tracker) + } + } + } + init { setupRecyclerView(binding.list) - binding.list.adapter = TrackersAdapter(viewModel::onClickTracker) + binding.list.adapter = adapter } - fun onBind(trackers: List) { - (binding.list.adapter as TrackersAdapter).dataSet = trackers + fun onBind(trackers: List) { + adapter.dataSet = trackers } } } 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 deleted file mode 100644 index 53b11b43..00000000 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2023 - 2024 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 -import foundation.e.advancedprivacy.trackers.domain.entities.Tracker - -class TrackersAdapter( - private val onClickTracker: (Tracker) -> Unit -) : - RecyclerView.Adapter() { - - class ViewHolder(view: View, private val onClickTracker: (Tracker) -> Unit) : 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 { - 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, onClickTracker) - } - - 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/TrackersPeriodFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt index 5f3acaf1..5c54d1cc 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt @@ -106,7 +106,9 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { override fun onResume() { super.onResume() lifecycleScope.launch { - binding.listsPager.currentItem = viewModel.getStartPosition() + viewModel.getStartPosition()?.let { + binding.listsPager.currentItem = it + } } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt index 61e47f99..587ef32e 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt @@ -109,9 +109,9 @@ class TrackersPeriodViewModel( _refreshUiHeight.emit(Unit) } - fun getStartPosition(): Int { + fun getStartPosition(): Int? { val startPosition = trackersScreenUseCase.getTrackerTabStartPosition() - return startPosition + return startPosition.takeIf { it in 0..1 } } sealed class SingleEvent { 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 9a1b8254..05492652 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 @@ -20,8 +20,8 @@ package foundation.e.advancedprivacy.features.trackers import androidx.annotation.StringRes import foundation.e.advancedprivacy.R -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import foundation.e.advancedprivacy.domain.entities.AppWithCount +import foundation.e.advancedprivacy.domain.entities.TrackerWithCount import java.time.Instant import java.time.ZonedDateTime import java.time.temporal.ChronoUnit @@ -35,9 +35,9 @@ data class TrackersPeriodState( val trackersCount: Int = 0, val trackersAllowedCount: Int = 0, val graduations: List? = null, - val allApps: List? = null, - val trackers: List? = null, - val appsWithTrackers: List? = null, + val allApps: List? = null, + val trackers: List? = null, + val appsWithTrackers: List? = null, val hideNoTrackersApps: Boolean = false ) { @@ -47,16 +47,6 @@ data class TrackersPeriodState( } } -data class AppWithTrackersCount( - val app: ApplicationDescription, - val trackersCount: Int = 0 -) - -data class TrackerWithAppsCount( - val tracker: Tracker, - val appsCount: Int = 0 -) - enum class TrackerTab(val position: Int) { APPS(0), TRACKERS(1) diff --git a/app/src/main/res/layout/dashboard_item_submenu_button.xml b/app/src/main/res/layout/dashboard_item_submenu_button.xml index a4454c2d..b826d05d 100644 --- a/app/src/main/res/layout/dashboard_item_submenu_button.xml +++ b/app/src/main/res/layout/dashboard_item_submenu_button.xml @@ -21,6 +21,7 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" + android:paddingHorizontal="16dp" android:background="?attr/selectableItemBackground"> @@ -120,7 +122,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="32dp" - android:paddingHorizontal="8dp" + android:paddingHorizontal="24dp" android:paddingVertical="12dp" android:orientation="horizontal" > @@ -128,15 +130,12 @@ android:layout_height="wrap_content" android:layout_width="0dp" android:layout_weight="1" - android:maxLines="1" - android:textColor="@color/primary_text" android:textSize="14sp" android:lineHeight="20dp" android:textFontWeight="400" android:textAllCaps="true" - android:text="@string/dashboard_apps_permissions_title" /> - Real IP address exposed Real IP address hidden Wall of shame + %s leaks detected View all apps + created %s leaks View all trackers 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 c3a6fe8b..79196c6c 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 @@ -396,7 +396,7 @@ class StatsDatabase( } } - suspend fun get5MostCalledTrackers(since: Long): List = withContext(Dispatchers.IO) { + suspend fun get5MostCalledTrackers(since: Long): List> = withContext(Dispatchers.IO) { synchronized(lock) { val db = readableDatabase val selection = "$COLUMN_NAME_TIMESTAMP >= ?" @@ -411,9 +411,11 @@ class StatsDatabase( " LIMIT 5", selectionArg ) - val trackers = mutableListOf() + val trackers = mutableListOf>() while (cursor.moveToNext()) { - trackers.add(cursor.getString(COLUMN_NAME_TRACKER)) + val trackerId = cursor.getString(COLUMN_NAME_TRACKER) + val calls = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) + trackers.add(trackerId to calls) } cursor.close() db.close() -- GitLab From f4c4af895ce3722307878e6f9b68f0697853cb5b Mon Sep 17 00:00:00 2001 From: jacquarg Date: Tue, 16 Apr 2024 10:34:37 +0200 Subject: [PATCH 3/4] feat:1960(WallOfShame):MR fixes. --- .../advancedprivacy/common/BindingAdapter.kt | 17 +++++++++++++ .../{AnyExtension.kt => IntExtensions.kt} | 3 +++ .../usecases/TrackersAndAppsListsUseCase.kt | 24 ++++++++++++------- .../domain/usecases/TrackersScreenUseCase.kt | 12 ++++++---- .../dashboard/ShameListsTabPagerAdapter.kt | 10 +++----- .../features/trackers/ListsTabPagerAdapter.kt | 10 +++----- 6 files changed, 50 insertions(+), 26 deletions(-) rename app/src/main/java/foundation/e/advancedprivacy/common/extensions/{AnyExtension.kt => IntExtensions.kt} (86%) diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt index 62944c8b..fffa671d 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2024 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 import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/IntExtensions.kt similarity index 86% rename from app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt rename to app/src/main/java/foundation/e/advancedprivacy/common/extensions/IntExtensions.kt index 652aefd8..537b891f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/IntExtensions.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2024 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -20,3 +21,5 @@ package foundation.e.advancedprivacy.common.extensions import android.content.Context fun Int.dpToPxF(context: Context): Float = this.toFloat() * context.resources.displayMetrics.density + +fun Int.dpToPx(context: Context): Int = (this * context.resources.displayMetrics.density).toInt() 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 index 62e37ae2..b02f43b6 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt @@ -34,12 +34,12 @@ class TrackersAndAppsListsUseCase( private val appListsRepository: AppListsRepository ) { suspend fun getAppsAndTrackersCounts(period: Period): TrackersAndAppsLists { - val (countByApp, countByTracker) = getCountByEntityMaps(period) + val countByEntitiesMaps = getCountByEntityMaps(period) return TrackersAndAppsLists( - trackers = buildTrackerList(countByTracker), - allApps = buildAllAppList(countByApp), - appsWithTrackers = buildAppList(countByApp) + trackers = buildTrackerList(countByEntitiesMaps.countByTrackers), + allApps = buildAllAppList(countByEntitiesMaps.countByApps), + appsWithTrackers = buildAppList(countByEntitiesMaps.countByApps) ) } @@ -73,7 +73,7 @@ class TrackersAndAppsListsUseCase( } } - private suspend fun getCountByEntityMaps(period: Period): Pair, Map> { + private suspend fun getCountByEntityMaps(period: Period): CountByEntitiesMaps { val periodStart: Instant = period.getPeriodStart() val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart) val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) @@ -110,15 +110,23 @@ class TrackersAndAppsListsUseCase( }.distinct() } - private fun foldToCountByEntityMaps( - trackersAndApps: List> - ): Pair, Map> { + private fun foldToCountByEntityMaps(trackersAndApps: List>): CountByEntitiesMaps { 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 + }.let { (countByApp, countByTracker) -> + CountByEntitiesMaps( + countByApps = countByApp, + countByTrackers = countByTracker + ) } } + + private data class CountByEntitiesMaps( + val countByApps: Map, + val countByTrackers: Map + ) } diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt index 93e398da..6c653b34 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt @@ -18,16 +18,20 @@ package foundation.e.advancedprivacy.domain.usecases import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class TrackersScreenUseCase(private val localStateRepository: LocalStateRepository) { +class TrackersScreenUseCase( + private val localStateRepository: LocalStateRepository, + private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO +) { - suspend fun getLastPosition(): Int = withContext(Dispatchers.IO) { + suspend fun getLastPosition(): Int = withContext(backgroundDispatcher) { localStateRepository.trackersScreenLastPosition } - suspend fun savePosition(currentPosition: Int) = withContext(Dispatchers.IO) { + suspend fun savePosition(currentPosition: Int) = withContext(backgroundDispatcher) { localStateRepository.trackersScreenLastPosition = currentPosition } @@ -39,7 +43,7 @@ class TrackersScreenUseCase(private val localStateRepository: LocalStateReposito localStateRepository.trackersScreenTabStartPosition = -1 } - suspend fun preselectTab(periodPosition: Int, tabPosition: Int) = withContext(Dispatchers.IO) { + suspend fun preselectTab(periodPosition: Int, tabPosition: Int) = withContext(backgroundDispatcher) { localStateRepository.trackersScreenLastPosition = periodPosition localStateRepository.trackersScreenTabStartPosition = tabPosition } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt index 880a0297..890fb2f4 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt @@ -16,7 +16,6 @@ */ package foundation.e.advancedprivacy.features.dashboard -import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -29,6 +28,7 @@ import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.BigNumberFormatter import foundation.e.advancedprivacy.common.BindingListAdapter import foundation.e.advancedprivacy.common.BindingViewHolder +import foundation.e.advancedprivacy.common.extensions.dpToPx import foundation.e.advancedprivacy.databinding.DashboardShameListBinding import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding import foundation.e.advancedprivacy.domain.entities.AppWithCount @@ -85,17 +85,13 @@ class ShameListsTabPagerAdapter( addItemDecoration( MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply { dividerColor = ContextCompat.getColor(context, R.color.divider) - dividerInsetStart = 16.dpToPx() - dividerInsetEnd = 16.dpToPx() + dividerInsetStart = 16.dpToPx(context) + dividerInsetEnd = 16.dpToPx(context) } ) } } - private fun Int.dpToPx(): Int { - return (this * Resources.getSystem().displayMetrics.density).toInt() - } - class AppsListViewHolder( private val binding: DashboardShameListBinding, private val viewModel: DashboardViewModel 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 index 10aeae75..7458f8e6 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt @@ -17,7 +17,6 @@ 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 @@ -29,6 +28,7 @@ import com.google.android.material.divider.MaterialDividerItemDecoration import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.BindingListAdapter import foundation.e.advancedprivacy.common.BindingViewHolder +import foundation.e.advancedprivacy.common.extensions.dpToPx import foundation.e.advancedprivacy.databinding.TrackersAppsListBinding import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding import foundation.e.advancedprivacy.databinding.TrackersListBinding @@ -93,17 +93,13 @@ class ListsTabPagerAdapter( addItemDecoration( MaterialDividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply { dividerColor = ContextCompat.getColor(context, R.color.divider) - dividerInsetStart = 16.dpToPx() - dividerInsetEnd = 16.dpToPx() + dividerInsetStart = 16.dpToPx(context) + dividerInsetEnd = 16.dpToPx(context) } ) } } - private fun Int.dpToPx(): Int { - return (this * Resources.getSystem().displayMetrics.density).toInt() - } - class AppsListViewHolder( private val binding: TrackersAppsListBinding, private val viewModel: TrackersPeriodViewModel -- GitLab From aefc74c2577fd572d45a9dc98582aad6646dec6f Mon Sep 17 00:00:00 2001 From: jacquarg Date: Tue, 16 Apr 2024 18:41:14 +0200 Subject: [PATCH 4/4] feat:1960(WallOfShame): remove dependences in Adapters, ifx app icons not loading on first start. --- .../e/advancedprivacy/KoinModule.kt | 4 +- .../features/dashboard/DashboardFragment.kt | 7 +++- .../dashboard/ShameListsTabPagerAdapter.kt | 29 ++++++++------- .../features/trackers/ListsTabPagerAdapter.kt | 37 +++++++++---------- .../trackers/TrackersPeriodFragment.kt | 8 +++- .../data/repositories/AppListsRepository.kt | 21 ++++++++--- 6 files changed, 65 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index c11260e7..938e5a15 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -146,7 +146,9 @@ val appModule = module { singleOf(::AppTrackersUseCase) singleOf(::TrackerDetailsUseCase) - singleOf(::TrackersScreenUseCase) + single { + TrackersScreenUseCase(localStateRepository = get()) + } single { PermissionsPrivacyModuleImpl(context = androidContext()) diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt index 8e2e5811..a8c17ec5 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt @@ -83,7 +83,12 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { binding.fakeLocation.title.setText(R.string.dashboard_location_title) binding.ipScrambling.title.setText(R.string.dashboard_ipscrambling_title) - tabAdapter = ShameListsTabPagerAdapter(viewModel) + tabAdapter = ShameListsTabPagerAdapter( + onClickShameApp = viewModel::onClickShameApp, + onClickShameTracker = viewModel::onClickShameTracker, + onClickViewAllApps = viewModel::onClickViewAllApps, + onClickViewAllTrackers = viewModel::onClickViewAllTrackers + ) binding.listsPager.adapter = tabAdapter diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt index 890fb2f4..4ff00958 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt @@ -32,11 +32,16 @@ import foundation.e.advancedprivacy.common.extensions.dpToPx import foundation.e.advancedprivacy.databinding.DashboardShameListBinding import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding import foundation.e.advancedprivacy.domain.entities.AppWithCount +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.TrackerWithCount import foundation.e.advancedprivacy.features.trackers.TrackerTab +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker class ShameListsTabPagerAdapter( - private val viewModel: DashboardViewModel + private val onClickShameApp: (ApplicationDescription) -> Unit, + private val onClickShameTracker: (Tracker) -> Unit, + private val onClickViewAllApps: () -> Unit, + private val onClickViewAllTrackers: () -> Unit ) : RecyclerView.Adapter() { private var uiState: DashboardState = DashboardState() @@ -51,10 +56,10 @@ class ShameListsTabPagerAdapter( val view = DashboardShameListBinding.inflate(LayoutInflater.from(parent.context), parent, false) return when (viewType) { TrackerTab.APPS.position -> { - ListsTabViewHolder.AppsListViewHolder(view, viewModel) + ListsTabViewHolder.AppsListViewHolder(view, onClickShameApp, onClickViewAllApps) } else -> { - ListsTabViewHolder.TrackersListViewHolder(view, viewModel) + ListsTabViewHolder.TrackersListViewHolder(view, onClickShameTracker, onClickViewAllTrackers) } } } @@ -94,7 +99,8 @@ class ShameListsTabPagerAdapter( class AppsListViewHolder( private val binding: DashboardShameListBinding, - private val viewModel: DashboardViewModel + private val onClickShameApp: (ApplicationDescription) -> Unit, + private val onClickViewAllApps: () -> Unit ) : ListsTabViewHolder(binding.root) { private val adapter = object : BindingListAdapter() { @@ -116,9 +122,7 @@ class ShameListsTabPagerAdapter( R.string.dashboard_wall_of_shame_app_calls, numberFormatter.format(item.count) ) - holder.binding.root.setOnClickListener { - viewModel.onClickShameApp(item.app) - } + holder.binding.root.setOnClickListener { onClickShameApp(item.app) } } } init { @@ -126,7 +130,7 @@ class ShameListsTabPagerAdapter( binding.list.adapter = adapter binding.viewAll.apply { text = binding.root.context.getString(R.string.dashboard_view_all_apps) - setOnClickListener { viewModel.onClickViewAllApps() } + setOnClickListener { onClickViewAllApps() } } } @@ -137,7 +141,8 @@ class ShameListsTabPagerAdapter( class TrackersListViewHolder( private val binding: DashboardShameListBinding, - private val viewModel: DashboardViewModel + private val onClickShameTracker: (Tracker) -> Unit, + private val onClickViewAllTrackers: () -> Unit ) : ListsTabViewHolder(binding.root) { private val adapter = object : BindingListAdapter() { @@ -159,9 +164,7 @@ class ShameListsTabPagerAdapter( R.string.dashboard_wall_of_shame_trackers_calls, numberFormatter.format(item.count) ) - holder.binding.root.setOnClickListener { - viewModel.onClickShameTracker(item.tracker) - } + holder.binding.root.setOnClickListener { onClickShameTracker(item.tracker) } } } @@ -170,7 +173,7 @@ class ShameListsTabPagerAdapter( binding.list.adapter = adapter binding.viewAll.apply { text = binding.root.context.getString(R.string.dashboard_view_all_trackers) - setOnClickListener { viewModel.onClickViewAllTrackers() } + setOnClickListener { onClickViewAllTrackers() } } } 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 index 7458f8e6..66d8e71e 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt @@ -16,7 +16,6 @@ */ package foundation.e.advancedprivacy.features.trackers -import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -33,14 +32,14 @@ import foundation.e.advancedprivacy.databinding.TrackersAppsListBinding import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding import foundation.e.advancedprivacy.databinding.TrackersListBinding import foundation.e.advancedprivacy.domain.entities.AppWithCount +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.TrackerWithCount - -const val TAB_APPS = 0 -private const val TAB_TRACKERS = 1 +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker class ListsTabPagerAdapter( - private val context: Context, - private val viewModel: TrackersPeriodViewModel + private val onClickTracker: (Tracker) -> Unit, + private val onClickApp: (ApplicationDescription) -> Unit, + private val onToggleHideNoTrackersApps: () -> Unit ) : RecyclerView.Adapter() { private var uiState: TrackersPeriodState = TrackersPeriodState() @@ -54,16 +53,17 @@ class ListsTabPagerAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListsTabViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { - TAB_APPS -> { + TrackerTab.APPS.position -> { ListsTabViewHolder.AppsListViewHolder( TrackersAppsListBinding.inflate(inflater, parent, false), - viewModel + onClickApp, + onToggleHideNoTrackersApps ) } else -> { ListsTabViewHolder.TrackersListViewHolder( TrackersListBinding.inflate(inflater, parent, false), - viewModel + onClickTracker ) } } @@ -75,10 +75,10 @@ class ListsTabPagerAdapter( override fun onBindViewHolder(holder: ListsTabViewHolder, position: Int) { when (position) { - TAB_APPS -> { + TrackerTab.APPS.position -> { (holder as ListsTabViewHolder.AppsListViewHolder).onBind(uiState) } - TAB_TRACKERS -> { + TrackerTab.TRACKERS.position -> { (holder as ListsTabViewHolder.TrackersListViewHolder).onBind(uiState.trackers ?: emptyList()) } } @@ -102,7 +102,8 @@ class ListsTabPagerAdapter( class AppsListViewHolder( private val binding: TrackersAppsListBinding, - private val viewModel: TrackersPeriodViewModel + private val onClickApp: (ApplicationDescription) -> Unit, + private val onToggleHideNoTrackersApps: () -> Unit ) : ListsTabViewHolder(binding.root) { private val adapter = object : BindingListAdapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { @@ -123,7 +124,7 @@ class ListsTabPagerAdapter( R.string.trackers_list_app_trackers_counts, item.count.toString() ) holder.binding.root.setOnClickListener { - viewModel.onClickApp(item.app) + onClickApp(item.app) } } } @@ -131,7 +132,7 @@ class ListsTabPagerAdapter( init { setupRecyclerView(binding.list) binding.list.adapter = adapter - binding.toggleNoTrackerApps.setOnClickListener { viewModel.onToggleHideNoTrackersApps() } + binding.toggleNoTrackerApps.setOnClickListener { onToggleHideNoTrackersApps() } } fun onBind(uiState: TrackersPeriodState) { @@ -151,8 +152,8 @@ class ListsTabPagerAdapter( } class TrackersListViewHolder( - private val binding: TrackersListBinding, - private val viewModel: TrackersPeriodViewModel + binding: TrackersListBinding, + private val onClickTracker: (Tracker) -> Unit ) : ListsTabViewHolder(binding.root) { private val adapter = object : BindingListAdapter() { @@ -173,9 +174,7 @@ class ListsTabPagerAdapter( holder.binding.counts.text = itemView.context.getString( R.string.trackers_list_tracker_apps_counts, item.count.toString() ) - holder.binding.root.setOnClickListener { - viewModel.onClickTracker(item.tracker) - } + holder.binding.root.setOnClickListener { onClickTracker(item.tracker) } } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt index 5c54d1cc..91f53394 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt @@ -79,13 +79,17 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { graphHolder = GraphHolder(binding.graphContainer) - tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel) + tabAdapter = ListsTabPagerAdapter( + onClickApp = viewModel::onClickApp, + onClickTracker = viewModel::onClickTracker, + onToggleHideNoTrackersApps = viewModel::onToggleHideNoTrackersApps + ) binding.listsPager.adapter = tabAdapter TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position -> tab.text = getString( when (position) { - TAB_APPS -> R.string.trackers_toggle_list_apps + TrackerTab.APPS.position -> R.string.trackers_toggle_list_apps else -> R.string.trackers_toggle_list_trackers } ) 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 6241d874..bb209d89 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 @@ -120,6 +120,7 @@ class AppListsRepository( private var lastFetchApps = 0 private var refreshAppJob: Job? = null + private fun refreshAppDescriptions(fetchMissingIcons: Boolean = true, force: Boolean = false): Job? { if (refreshAppJob == null || refreshAppJob?.isCompleted == true) { refreshAppJob = coroutineScope.launch(Dispatchers.IO) { @@ -221,11 +222,21 @@ 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 + if (apId.isBlank()) return@withContext null + + refreshAppDescriptions() + + appsByAPId[apId]?.let { app -> + when (app) { + in getCompatibilityApps() -> dummyCompatibilityApp + in getHiddenSystemApps() -> dummySystemApp + else -> { + if (app.icon == null) { + app.copy(icon = permissionsModule.getApplicationIcon(app)) + } else { + app + } + } } } } -- GitLab