diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 230659115d2bd2d37b2659d3a16b73a1fcc1fe0a..938e5a15fea4b3327a65fd68e0dabe4aae7e7191 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 @@ -146,7 +146,9 @@ val appModule = module { singleOf(::AppTrackersUseCase) singleOf(::TrackerDetailsUseCase) - singleOf(::TrackersScreenUseCase) + single { + TrackersScreenUseCase(localStateRepository = get()) + } single { PermissionsPrivacyModuleImpl(context = androidContext()) @@ -185,7 +187,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/common/BindingAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..fffa671dda7f3a773a6c2ffc1ec70336e9c2fa4c --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/common/BindingAdapter.kt @@ -0,0 +1,33 @@ +/* + * 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 +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/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 652aefd8d56647947a3de3df5022a19a77b79989..537b891fd44d37b114ff29f584d1a727cde54ea9 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/data/repositories/LocalStateRepositoryImpl.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepositoryImpl.kt index c131eef58341658aa725b0044a0f18d4de0ce715..4bd5f2038be7a412fc81aa58035955a7ea93c33d 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/entities/TrackersAndAppsLists.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersAndAppsLists.kt index 2113b3a3f0e30837a5b1aeef330c2175cf10a2b4..e844473263f0dcb971eb1a35abc9a46804939038 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 5606d499926d16334f77e4e84a415f7f2ee6565e..b02f43b6c7833a96f4e6ea80f3bedf703b373469 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 @@ -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 @@ -34,34 +34,68 @@ 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 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) ) } - private fun buildTrackerList(countByTracker: Map): List { + suspend fun buildWallOfShame(): TrackersAndAppsLists { + val trackers = statsDatabase + .get5MostCalledTrackers(since = Period.MONTH.getPeriodStart().epochSecond) + .mapNotNull { (trackerId, calls) -> + trackersRepository.getTracker(trackerId)?.let { + TrackerWithCount(it, calls) + } + } + + return TrackersAndAppsLists( + trackers = trackers, + appsWithTrackers = get5MostTrackedAppsLastMonth(), + 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 { (app, count) -> + AppWithCount(app, count) + } + } + + private suspend fun getCountByEntityMaps(period: Period): CountByEntitiesMaps { + 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) - }.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> { @@ -76,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 16941443a2e1dff07583af381a28cab017e1a0d7..6c653b34fbce86cb2a5a56c2eaf7200ef867a069 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 @@ -18,16 +18,33 @@ 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 } + + fun getTrackerTabStartPosition(): Int { + return localStateRepository.trackersScreenTabStartPosition + } + + fun resetTrackerTabStartPosition() { + localStateRepository.trackersScreenTabStartPosition = -1 + } + + 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/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt index 765255fc0cb4597ff7fabbacb4e34364e7a87160..0b50c8e141d0b5ce23bd33f14a34b100cba5050a 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 4b98d6d057ddb58b1b2f40ec247e119115ae59ff..a8c17ec5a0b2ad3be6f9e5bdfa68ed422fd28c13 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,24 @@ 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( + onClickShameApp = viewModel::onClickShameApp, + onClickShameTracker = viewModel::onClickShameTracker, + onClickViewAllApps = viewModel::onClickViewAllApps, + onClickViewAllTrackers = viewModel::onClickViewAllTrackers + ) + + 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 +235,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 7dc3c0b50e1f4e2f176f1bf78f95c10f4c876f34..e53c88793d029c4a9e6754f4b5bb88188a0b7b1a 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 @@ -17,13 +18,17 @@ 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.domain.entities.TrackerWithCount 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 a820be6576114cb05071c15a6ddd4e9cf53add22..badf0ab9c3175cab525547f59560e2c0915b78ee 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.buildWallOfShame() _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 0000000000000000000000000000000000000000..4ff0095876ca473389e107a0769b960f13322829 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/ShameListsTabPagerAdapter.kt @@ -0,0 +1,185 @@ +/* + * 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.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.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 onClickShameApp: (ApplicationDescription) -> Unit, + private val onClickShameTracker: (Tracker) -> Unit, + private val onClickViewAllApps: () -> Unit, + private val onClickViewAllTrackers: () -> Unit +) : 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, onClickShameApp, onClickViewAllApps) + } + else -> { + ListsTabViewHolder.TrackersListViewHolder(view, onClickShameTracker, onClickViewAllTrackers) + } + } + } + + 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 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(context) + dividerInsetEnd = 16.dpToPx(context) + } + ) + } + } + + class AppsListViewHolder( + private val binding: DashboardShameListBinding, + private val onClickShameApp: (ApplicationDescription) -> Unit, + private val onClickViewAllApps: () -> Unit + ) : 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 { onClickShameApp(item.app) } + } + } + init { + setupRecyclerView(binding.list) + binding.list.adapter = adapter + binding.viewAll.apply { + text = binding.root.context.getString(R.string.dashboard_view_all_apps) + setOnClickListener { onClickViewAllApps() } + } + } + + fun onBind(uiState: DashboardState) { + adapter.dataSet = uiState.shameApps + } + } + + class TrackersListViewHolder( + private val binding: DashboardShameListBinding, + private val onClickShameTracker: (Tracker) -> Unit, + private val onClickViewAllTrackers: () -> Unit + ) : 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 { onClickShameTracker(item.tracker) } + } + } + + init { + setupRecyclerView(binding.list) + binding.list.adapter = adapter + binding.viewAll.apply { + text = binding.root.context.getString(R.string.dashboard_view_all_trackers) + setOnClickListener { onClickViewAllTrackers() } + } + } + + fun onBind(uiState: DashboardState) { + 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 dcfd81766085571161f0d22b161d65a72fab663c..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/AppsAdapter.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2022 - 2023 MURENA SAS - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * 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 - -class AppsAdapter( - private val viewModel: TrackersPeriodViewModel -) : - RecyclerView.Adapter() { - - class ViewHolder(view: View, private val parentViewModel: TrackersPeriodViewModel) : 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() - 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/ListsTabPagerAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/ListsTabPagerAdapter.kt index 566a9cb8ac0f73d8c80666e48a7c721ba88345dd..66d8e71edaa28038293bb08a8b1970580cac7447 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 @@ -16,25 +16,30 @@ */ package foundation.e.advancedprivacy.features.trackers -import android.content.Context -import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.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.common.extensions.dpToPx import foundation.e.advancedprivacy.databinding.TrackersAppsListBinding +import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding import foundation.e.advancedprivacy.databinding.TrackersListBinding - -const val TAB_APPS = 0 -private const val TAB_TRACKERS = 1 +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.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() @@ -48,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 ) } } @@ -69,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()) } } @@ -87,29 +93,50 @@ 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 + 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 { + 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 { + onClickApp(item.app) + } + } + } + init { setupRecyclerView(binding.list) - binding.list.adapter = AppsAdapter(viewModel) - binding.toggleNoTrackerApps.setOnClickListener { viewModel.onToggleHideNoTrackersApps() } + binding.list.adapter = adapter + binding.toggleNoTrackerApps.setOnClickListener { onToggleHideNoTrackersApps() } } fun onBind(uiState: TrackersPeriodState) { - (binding.list.adapter as AppsAdapter).dataSet = ( + adapter.dataSet = ( if (uiState.hideNoTrackersApps) { uiState.appsWithTrackers } else { @@ -125,16 +152,39 @@ 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() { + 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 { onClickTracker(item.tracker) } + } + } + init { setupRecyclerView(binding.list) - binding.list.adapter = TrackersAdapter(viewModel) + 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 135d43e07037467c3bf92baea2a0e1aa0f41a5d8..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersAdapter.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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: TrackersPeriodViewModel -) : - RecyclerView.Adapter() { - - class ViewHolder(view: View, private val parentViewModel: TrackersPeriodViewModel) : 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 6518de1faa01c3307e8344fdc47193917593b8b5..5f55b1a83c60d1df99d1c1442df0ea7944be81df 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 f051407df81283070ccbbe228519e77a5591a0fc..91f533944304528e40e6833584c4aa7e70084f0c 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 @@ -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 } ) @@ -103,6 +107,15 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { listenViewModel() } + override fun onResume() { + super.onResume() + lifecycleScope.launch { + viewModel.getStartPosition()?.let { + binding.listsPager.currentItem = it + } + } + } + @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 b708b0b0f008d803e107943965a9e9cf771dc10a..587ef32ea3c1ad119f93f753a6c5d8296872309d 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.takeIf { it in 0..1 } + } + 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 f0787fd8509853a41ef7b74e242334e220cb81e9..054926529efd3ad47f46e2b7ef788bb8930e051d 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 @@ -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,15 +47,10 @@ 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) +} enum class Period(val periodsCount: Int, val periodUnit: TemporalUnit) { DAY(24, ChronoUnit.HOURS), 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 945789e4c5c999143ecadedbb60ef36e4735c48b..b682adfbf2b7e664270b7a57c645b609f0d13869 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 a7cec14af9e3c27eb63c33b541d5aa7bbb1ebc83..cc989ff621c41b952952bde9f5810931f22f0fd7 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_item_submenu_button.xml b/app/src/main/res/layout/dashboard_item_submenu_button.xml index a4454c2dd52664803f44b86269e5fbc140b56a4b..b826d05d8d60ee451688ce9a700d6a65f3f684e4 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"> + + + + + + + diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index fd685c92705b2da3b518e92e7a89b05f1369a500..9dfc66e4e3526956f66f572da6423c9518d65409 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -1,4 +1,19 @@ - + @@ -104,7 +121,8 @@ android:id="@+id/apps_permissions" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="8dp" + android:layout_marginBottom="32dp" + android:paddingHorizontal="24dp" android:paddingVertical="12dp" android:orientation="horizontal" > @@ -112,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" /> - + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 83636df8d891f4f118fb9a6fad674bc14f90ade9..ea5bb2501c78fd04ceacbb1c0e2632570fdb7aff 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/data/repositories/AppListsRepository.kt b/core/src/main/java/foundation/e/advancedprivacy/data/repositories/AppListsRepository.kt index 6241d874c0f0fb0fb6ec778533dab9f7554faba8..bb209d89b80e6087a9e7102b25d779a8621f572b 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 + } + } } } } 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 86e1acf94c9616a4f7f25d80a655d27b9fab7924..39212a23e922d6175ae2a99b93f7b61bc8db609a 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 beaf109beb41aa48203bee077db395d410501998..79196c6cb92e58e26ed2b6b68f5bcab0f3e533f1 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,58 @@ 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()) { + val trackerId = cursor.getString(COLUMN_NAME_TRACKER) + val calls = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) + trackers.add(trackerId to calls) + } + cursor.close() + db.close() + trackers + } + } + suspend fun logAccess(trackerId: String?, appId: String, blocked: Boolean) = withContext( Dispatchers.IO ) {