diff --git a/app/build.gradle b/app/build.gradle index a10ead1cc061f636449224c82c3efc399c7a5502..1d05635019e6dd7a32381816eee4f5d5f7418875 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,24 +156,25 @@ dependencies { standaloneImplementation project(':trackersservicestandalone') implementation ( - libs.eos.elib, - libs.androidx.core.ktx, libs.androidx.appcompat, libs.androidx.fragment.ktx, libs.androidx.lifecycle.runtime, libs.androidx.lifecycle.viewmodel, + libs.androidx.navigation.fragment, + libs.androidx.navigation.ui, libs.androidx.viewpager2, + libs.bundles.koin, - libs.google.material, - libs.androidx.navigation.fragment, - libs.androidx.navigation.ui, + libs.eos.elib, + libs.eos.telemetry, + + libs.google.material, libs.maplibre, libs.mpandroidcharts, - libs.eos.telemetry, libs.timber ) diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 55183e9a40e2503741418432190226eae1043f39..4c7f18e391e98815c1e0f0e62dd4ef73bbf37825 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -21,6 +21,7 @@ import android.content.res.Resources import android.os.Process import foundation.e.advancedprivacy.core.coreModule import foundation.e.advancedprivacy.data.repositories.LocalStateRepositoryImpl +import foundation.e.advancedprivacy.data.repositories.ResourcesGetter import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.CHANNEL_TRACKER_FLAG import foundation.e.advancedprivacy.domain.entities.NotificationContent @@ -34,6 +35,7 @@ import foundation.e.advancedprivacy.domain.usecases.IpScramblingStateUseCase import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase import foundation.e.advancedprivacy.domain.usecases.TrackerDetailsUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase +import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase import foundation.e.advancedprivacy.dummy.CityDataSource @@ -42,6 +44,8 @@ import foundation.e.advancedprivacy.fakelocation.fakelocationModule import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel import foundation.e.advancedprivacy.features.internetprivacy.InternetPrivacyViewModel import foundation.e.advancedprivacy.features.location.FakeLocationViewModel +import foundation.e.advancedprivacy.features.trackers.Period +import foundation.e.advancedprivacy.features.trackers.TrackersPeriodViewModel import foundation.e.advancedprivacy.features.trackers.TrackersViewModel import foundation.e.advancedprivacy.features.trackers.apptrackers.AppTrackersViewModel import foundation.e.advancedprivacy.features.trackers.trackerdetails.TrackerDetailsViewModel @@ -110,6 +114,7 @@ val appModule = module { } single { CityDataSource } + single { ResourcesGetter(androidContext()) } singleOf(::AppListUseCase) single { @@ -141,6 +146,7 @@ val appModule = module { singleOf(::AppTrackersUseCase) singleOf(::TrackerDetailsUseCase) + singleOf(::TrackersScreenUseCase) single { PermissionsPrivacyModuleImpl(context = androidContext()) @@ -172,6 +178,17 @@ val appModule = module { ) } + viewModel { parameters -> + + val period: Period = runCatching { Period.valueOf(parameters.get()) }.getOrDefault(Period.DAY) + + TrackersPeriodViewModel( + period = period, + trackersStatisticsUseCase = get(), + trackersAndAppsListsUseCase = get() + ) + } + viewModelOf(::TrackersViewModel) viewModelOf(::FakeLocationViewModel) viewModelOf(::InternetPrivacyViewModel) diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt index 2afd6eea5bd71b76abcf91499918738ed616852c..c131eef58341658aa725b0044a0f18d4de0ce715 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt @@ -42,6 +42,7 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { private const val KEY_HIDE_WARNING_TRACKERS = "hide_warning_trackers" private const val KEY_HIDE_WARNING_LOCATION = "hide_warning_location" private const val KEY_HIDE_WARNING_IPSCRAMBLING = "hide_warning_ipscrambling" + private const val KEY_TRACKERS_SCREEN_LAST_POSITION = "trackers_screen_last_position" } private val sharedPref = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) @@ -125,6 +126,10 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { get() = sharedPref.getBoolean(KEY_HIDE_WARNING_IPSCRAMBLING, false) set(value) = set(KEY_HIDE_WARNING_IPSCRAMBLING, value) + override var trackersScreenLastPosition: Int + get() = sharedPref.getInt(KEY_TRACKERS_SCREEN_LAST_POSITION, 0) + set(value) = sharedPref.edit().putInt(KEY_TRACKERS_SCREEN_LAST_POSITION, value).apply() + private fun set(key: String, value: Boolean) { sharedPref.edit().putBoolean(key, value).apply() } diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt new file mode 100644 index 0000000000000000000000000000000000000000..6902a99397ae1a9c4bac6ca3c166d56a9a3db390 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt @@ -0,0 +1,47 @@ +/* + * 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.data.repositories + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import androidx.annotation.StringRes +import timber.log.Timber +import java.time.format.DateTimeFormatter +import java.util.Locale + +class ResourcesGetter(private val context: Context) { + private val defaultResources by lazy { getLocalizedResources(context, Locale("")) } + + private fun getLocalizedResources(context: Context, desiredLocale: Locale?): Resources { + var conf: Configuration = context.resources.configuration + conf = Configuration(conf) + conf.setLocale(desiredLocale) + val localizedContext = context.createConfigurationContext(conf) + return localizedContext.resources + } + + fun getFormatter(@StringRes formatRes: Int): DateTimeFormatter { + return runCatching { + DateTimeFormatter.ofPattern(context.getString(formatRes)) + }.getOrElse { + Timber.w(it, "Can't parse DateTimeFormatter") + DateTimeFormatter.ofPattern(defaultResources.getString(formatRes)) + } + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt index c0fa637b6925cbb462bd0ccbdf0079c449afcc8d..95c5c3d868aca181faf2aa1db394aab22c70b4c3 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 MURENA SAS * Copyright (C) 2022 E FOUNDATION * * This program is free software: you can redistribute it and/or modify @@ -21,5 +22,6 @@ data class TrackersPeriodicStatistics( val callsBlockedNLeaked: List>, val periods: List, val trackersCount: Int, + val trackersAllowedCount: Int = 0, val graduations: List? = null ) 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 ea07e8fa10a7f5d909a5601d490bcbe0fb9bbcf6..9bff2f7e85091345ff774c7f437b809ad1e1759f 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 @@ -20,20 +20,22 @@ import foundation.e.advancedprivacy.data.repositories.AppListsRepository import foundation.e.advancedprivacy.domain.entities.ApplicationDescription 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 import kotlinx.coroutines.flow.first +import java.time.Instant class TrackersAndAppsListsUseCase( private val statsDatabase: StatsDatabase, private val trackersRepository: TrackersRepository, private val appListsRepository: AppListsRepository, ) { - - suspend fun getTrackersAndAppsLists(): TrackersAndAppsLists { - val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp() + 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) 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 new file mode 100644 index 0000000000000000000000000000000000000000..16941443a2e1dff07583af381a28cab017e1a0d7 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.domain.usecases + +import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class TrackersScreenUseCase(private val localStateRepository: LocalStateRepository) { + + suspend fun getLastPosition(): Int = withContext(Dispatchers.IO) { + localStateRepository.trackersScreenLastPosition + } + + suspend fun savePosition(currentPosition: Int) = withContext(Dispatchers.IO) { + localStateRepository.trackersScreenLastPosition = currentPosition + } +} 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 8f290b85437e47b41897568b0bc8271c6599f20e..804ba3812469bf49ffb1c517dd5a5c6ac522d0d1 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 @@ -18,12 +18,13 @@ package foundation.e.advancedprivacy.domain.usecases -import android.content.res.Resources import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.throttleFirst import foundation.e.advancedprivacy.data.repositories.AppListsRepository +import foundation.e.advancedprivacy.data.repositories.ResourcesGetter import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics +import foundation.e.advancedprivacy.features.trackers.Period import foundation.e.advancedprivacy.trackers.data.StatsDatabase import foundation.e.advancedprivacy.trackers.data.TrackersRepository import foundation.e.advancedprivacy.trackers.data.WhitelistRepository @@ -35,7 +36,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -46,7 +46,7 @@ class TrackersStatisticsUseCase( private val trackersRepository: TrackersRepository, private val appListsRepository: AppListsRepository, private val statsDatabase: StatsDatabase, - private val resources: Resources + private val resourcesGetter: ResourcesGetter, ) { fun initAppList() { appListsRepository.apps() @@ -92,10 +92,16 @@ class TrackersStatisticsUseCase( fun getDayTrackersCount() = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS) + private fun buildGraduations(period: Period): List { + return when (period) { + Period.DAY -> buildDayGraduations() + Period.MONTH -> buildMonthGraduations() + Period.YEAR -> buildYearGraduations() + } + } + private fun buildDayGraduations(): List { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_hours_period_format) - ) + val formatter = resourcesGetter.getFormatter(R.string.trackers_graph_hours_period_format) val periods = mutableListOf() var end = ZonedDateTime.now() @@ -107,10 +113,49 @@ class TrackersStatisticsUseCase( return periods.reversed() } - private fun buildDayLabels(): List { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_hours_period_format) + private fun buildMonthGraduations(): List { + val formatter = resourcesGetter.getFormatter( + R.string.trackers_graph_month_graduations_format ) + + val periods = mutableListOf() + var end = ZonedDateTime.now() + for (i in 1..30) { + val start = end.truncatedTo(ChronoUnit.DAYS) + periods.add(if ((start.dayOfMonth) % 6 == 0) formatter.format(start) else null) + end = start.minus(1, ChronoUnit.HOURS) + } + + return periods.reversed() + } + + private fun buildYearGraduations(): List { + val formatter = resourcesGetter.getFormatter(R.string.trackers_graph_year_graduations_format) + + val periods = mutableListOf() + var end = ZonedDateTime.now() + for (i in 1..12) { + val start = end.truncatedTo(ChronoUnit.DAYS).let { + it.minusDays(it.dayOfMonth.toLong()) + } + periods.add(if (start.monthValue % 3 == 0) formatter.format(start) else null) + end = start.minus(1, ChronoUnit.DAYS) + } + + return periods.reversed() + } + + private fun buildLabels(period: Period): List { + return when (period) { + Period.DAY -> buildDayLabels() + Period.MONTH -> buildMonthLabels() + Period.YEAR -> buildYearLabels() + } + } + + private fun buildDayLabels(): List { + val formatter = resourcesGetter.getFormatter(R.string.trackers_graph_hours_period_format) + val periods = mutableListOf() var end = ZonedDateTime.now() for (i in 1..24) { @@ -122,9 +167,8 @@ class TrackersStatisticsUseCase( } private fun buildMonthLabels(): List { - val formater = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_days_period_format) - ) + val formater = resourcesGetter.getFormatter(R.string.trackers_graph_days_period_format) + val periods = mutableListOf() var day = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS) for (i in 1..30) { @@ -135,9 +179,8 @@ class TrackersStatisticsUseCase( } private fun buildYearLabels(): List { - val formater = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_months_period_format) - ) + val formater = resourcesGetter.getFormatter(R.string.trackers_graph_months_period_format) + val periods = mutableListOf() var month = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1) for (i in 1..12) { @@ -147,23 +190,13 @@ class TrackersStatisticsUseCase( return periods.reversed() } - fun getDayMonthYearStatistics(): Triple { - return Triple( - TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS), - periods = buildDayLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS) - ), - TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(30, ChronoUnit.DAYS), - periods = buildMonthLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(30, ChronoUnit.DAYS) - ), - TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS), - periods = buildYearLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS) - ) + suspend fun getGraphData(period: Period): TrackersPeriodicStatistics { + return TrackersPeriodicStatistics( + callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(period.periodsCount, period.periodUnit), + periods = buildLabels(period), + trackersCount = statsDatabase.getTrackersCount(period.periodsCount, period.periodUnit), + trackersAllowedCount = statsDatabase.getLeakedTrackersCount(period.periodsCount, period.periodUnit), + graduations = buildGraduations(period), ) } 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 f00dff8f5c833fbffe2e905f2ff80b8ea5e2399f..dcfd81766085571161f0d22b161d65a72fab663c 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,6 @@ /* - * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS + * Copyright (C) 2022 - 2023 MURENA SAS + * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,11 +26,11 @@ import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding class AppsAdapter( - private val viewModel: TrackersViewModel + private val viewModel: TrackersPeriodViewModel ) : RecyclerView.Adapter() { - class ViewHolder(view: View, private val parentViewModel: TrackersViewModel) : RecyclerView.ViewHolder(view) { + 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) 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 e9a046f73ea0c2d77216987c3ff96a9303284ed9..dd665fc9f35badb9f977f1d0f5f05cc00ab996f8 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 @@ -34,11 +34,11 @@ private const val TAB_TRACKERS = 1 class ListsTabPagerAdapter( private val context: Context, - private val viewModel: TrackersViewModel, + private val viewModel: TrackersPeriodViewModel, ) : RecyclerView.Adapter() { - private var uiState: TrackersState = TrackersState() + private var uiState: TrackersPeriodState = TrackersPeriodState() - fun updateDataSet(state: TrackersState) { + fun updateDataSet(state: TrackersPeriodState) { uiState = state notifyDataSetChanged() } @@ -99,7 +99,7 @@ class ListsTabPagerAdapter( class AppsListViewHolder( private val binding: TrackersAppsListBinding, - private val viewModel: TrackersViewModel + private val viewModel: TrackersPeriodViewModel ) : ListsTabViewHolder(binding.root) { init { setupRecyclerView(binding.list) @@ -107,7 +107,7 @@ class ListsTabPagerAdapter( binding.toggleNoTrackerApps.setOnClickListener { viewModel.onToggleHideNoTrackersApps() } } - fun onBind(uiState: TrackersState) { + fun onBind(uiState: TrackersPeriodState) { (binding.list.adapter as AppsAdapter).dataSet = ( if (uiState.hideNoTrackersApps) uiState.appsWithTrackers else uiState.allApps @@ -122,7 +122,7 @@ class ListsTabPagerAdapter( class TrackersListViewHolder( private val binding: TrackersListBinding, - private val viewModel: TrackersViewModel + private val viewModel: TrackersPeriodViewModel ) : ListsTabViewHolder(binding.root) { init { setupRecyclerView(binding.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 3270bf32ff3e56c9c0a2655f4afc777f86526d0e..135d43e07037467c3bf92baea2a0e1aa0f41a5d8 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 @@ -26,11 +26,11 @@ import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.databinding.TrackersItemAppBinding class TrackersAdapter( - val viewModel: TrackersViewModel + val viewModel: TrackersPeriodViewModel ) : RecyclerView.Adapter() { - class ViewHolder(view: View, private val parentViewModel: TrackersViewModel) : RecyclerView.ViewHolder(view) { + class ViewHolder(view: View, private val parentViewModel: TrackersPeriodViewModel) : RecyclerView.ViewHolder(view) { val binding = TrackersItemAppBinding.bind(view) init { binding.icon.isVisible = false 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 b88c55ea21b100a099deda3d1aaee19964f78eb8..063458dac192ff2e9dda555413b588da578c9dda 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 @@ -31,7 +31,6 @@ import android.view.View import android.view.ViewTreeObserver import android.widget.Toast import androidx.core.content.ContextCompat -import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -39,13 +38,10 @@ import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import foundation.e.advancedprivacy.R -import foundation.e.advancedprivacy.common.GraphHolder import foundation.e.advancedprivacy.common.NavToolbarFragment import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPosition import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding -import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding -import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel @@ -54,56 +50,46 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private lateinit var binding: FragmentTrackersBinding - private var dayGraphHolder: GraphHolder? = null - private var monthGraphHolder: GraphHolder? = null - private var yearGraphHolder: GraphHolder? = null - - private lateinit var tabAdapter: ListsTabPagerAdapter + private lateinit var pagerAdapter: TrackersPeriodAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentTrackersBinding.bind(view) - dayGraphHolder = GraphHolder(binding.graphDay.graph, requireContext(), false) - monthGraphHolder = GraphHolder(binding.graphMonth.graph, requireContext(), false) - yearGraphHolder = GraphHolder(binding.graphYear.graph, requireContext(), false) - - tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel) - binding.listsPager.adapter = tabAdapter + val trackersTabs = binding.trackersPeriodsTabs + val trackersPager = binding.trackersPeriodsPager - TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position -> - tab.text = getString( - when (position) { - TAB_APPS -> R.string.trackers_toggle_list_apps - else -> R.string.trackers_toggle_list_trackers - } - ) + pagerAdapter = TrackersPeriodAdapter(this, viewModel) + trackersPager.adapter = pagerAdapter + TabLayoutMediator(trackersTabs, trackersPager) { tab, position -> + tab.text = getString(viewModel.getDisplayDuration(position)) }.attach() - binding.listsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + setupTrackersInfos() + + binding.trackersPeriodsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageScrollStateChanged(state: Int) { super.onPageScrollStateChanged(state) if (state == ViewPager2.SCROLL_STATE_IDLE) { updatePagerHeight() + viewModel.onDisplayedItemChanged(binding.trackersPeriodsPager.currentItem) } } }) - setupTrackersInfos() - listenViewModel() } + override fun onResume() { + super.onResume() + lifecycleScope.launch { + binding.trackersPeriodsPager.currentItem = viewModel.getLastPosition() + } + } + private fun listenViewModel() { with(viewLifecycleOwner) { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - render(viewModel.state.value) - viewModel.state.collect(::render) - } - } - lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.singleEvents.collect(::handleEvents) @@ -115,12 +101,6 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { viewModel.navigate.collect(findNavController()::navigate) } } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } } } @@ -176,17 +156,17 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private var oldPosition = -1 private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { - binding.listsPager.findViewHolderForAdapterPosition(binding.listsPager.currentItem) + binding.trackersPeriodsPager.findViewHolderForAdapterPosition(binding.trackersPeriodsPager.currentItem) .let { currentViewHolder -> - currentViewHolder?.itemView?.let { binding.listsPager.updatePagerHeightForChild(it) } + currentViewHolder?.itemView?.let { binding.trackersPeriodsPager.updatePagerHeightForChild(it) } } } private fun updatePagerHeight() { - with(binding.listsPager) { + with(binding.trackersPeriodsPager) { val position = currentItem if (position == oldPosition) return - if (oldPosition > 0) { + if (oldPosition >= 0) { val oldItem = findViewHolderForAdapterPosition(oldPosition)?.itemView oldItem?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) } @@ -195,51 +175,12 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener) oldPosition = position - adapter?.notifyItemChanged(position) + + binding.trackersPeriodsPager.requestLayout() } } private fun displayToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } - - private fun render(state: TrackersState) { - state.dayStatistics?.let { renderGraph(it, dayGraphHolder!!, binding.graphDay) } - state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) } - state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) } - updatePagerHeight() - - tabAdapter.updateDataSet(state) - } - - private fun renderGraph( - statistics: TrackersPeriodicStatistics, - graphHolder: GraphHolder, - graphBinding: TrackersItemGraphBinding - ) { - if (statistics.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) { - graphBinding.graph.visibility = View.INVISIBLE - graphBinding.graphEmpty.isVisible = true - } else { - graphBinding.graph.isVisible = true - graphBinding.graphEmpty.isVisible = false - graphHolder.data = statistics.callsBlockedNLeaked - graphHolder.labels = statistics.periods - graphBinding.trackersCountLabel.text = - getString(R.string.trackers_count_label, statistics.trackersCount) - } - } - - override fun onDestroyView() { - super.onDestroyView() - kotlin.runCatching { - if (oldPosition >= 0) { - val oldItem = binding.listsPager.findViewHolderForAdapterPosition(oldPosition) - oldItem?.itemView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) - } - } - dayGraphHolder = null - monthGraphHolder = null - yearGraphHolder = null - } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..6afc1ba86d9cff24d554df67e4bd3e8ce81d5eb6 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt @@ -0,0 +1,20 @@ +package foundation.e.advancedprivacy.features.trackers + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter + +class TrackersPeriodAdapter( + context: Fragment, + private val viewModel: TrackersViewModel +) : FragmentStateAdapter(context) { + + override fun getItemCount(): Int { + return viewModel.positionsCount + } + + override fun createFragment(position: Int): Fragment { + return TrackersPeriodFragment().apply { + arguments = TrackersPeriodFragment.buildArguments(period = viewModel.getPeriod(position)) + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..995bda36a546e05fa9b442da80164e994ddd8779 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt @@ -0,0 +1,194 @@ +/* + * 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.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.ViewTreeObserver +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.tabs.TabLayoutMediator +import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPosition +import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild +import foundation.e.advancedprivacy.databinding.TrackersPeriodFragmentBinding +import foundation.e.advancedprivacy.features.trackers.TrackersPeriodViewModel.SingleEvent +import foundation.e.advancedprivacy.features.trackers.graph.GraphHolder +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf + +class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { + + companion object { + private const val ARG_PERIOD = "period" + + fun buildArguments(period: Period): Bundle { + return Bundle().apply { + putString(ARG_PERIOD, period.name) + } + } + } + + private val viewModel: TrackersPeriodViewModel by viewModel { + parametersOf( + requireArguments().getString( + ARG_PERIOD + ) + ) + } + + private lateinit var binding: TrackersPeriodFragmentBinding + + private lateinit var tabAdapter: ListsTabPagerAdapter + private lateinit var graphHolder: GraphHolder + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding = TrackersPeriodFragmentBinding.bind(view) + + graphHolder = GraphHolder(binding.graphContainer) + + tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel) + binding.listsPager.adapter = tabAdapter + + TabLayoutMediator(binding.listsTabs, binding.listsPager) { tab, position -> + tab.text = getString( + when (position) { + TAB_APPS -> R.string.trackers_toggle_list_apps + else -> R.string.trackers_toggle_list_trackers + } + ) + }.attach() + + binding.listsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageScrollStateChanged(state: Int) { + super.onPageScrollStateChanged(state) + if (state == ViewPager2.SCROLL_STATE_IDLE) { + updatePagerHeight() + } + } + }) + + listenViewModel() + } + + private fun listenViewModel() { + with(viewLifecycleOwner) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + render(viewModel.state.value) + viewModel.state.collect(::render) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.singleEvents.collect(::handleEvents) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.navigate.collect(findNavController()::navigate) + } + } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.doOnStartedState() + } + } + } + } + + private fun handleEvents(event: SingleEvent) { + when (event) { + is SingleEvent.ErrorEvent -> { + displayToast(event.error) + } + is SingleEvent.OpenUrl -> { + try { + startActivity(Intent(Intent.ACTION_VIEW, event.url)) + } catch (e: ActivityNotFoundException) { + Toast.makeText( + requireContext(), + R.string.error_no_activity_view_url, + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + private var oldPosition = -1 + private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { + binding.listsPager.findViewHolderForAdapterPosition(binding.listsPager.currentItem) + .let { currentViewHolder -> + currentViewHolder?.itemView?.let { binding.listsPager.updatePagerHeightForChild(it) } + } + } + + private fun updatePagerHeight() { + with(binding.listsPager) { + val position = currentItem + if (position == oldPosition) return + if (oldPosition >= 0) { + val oldItem = findViewHolderForAdapterPosition(oldPosition)?.itemView + oldItem?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) + } + + val newItem = findViewHolderForAdapterPosition(position)?.itemView + newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener) + + oldPosition = position + binding.listsPager.requestLayout() + } + } + + private fun displayToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + + @SuppressLint("NotifyDataSetChanged") + private fun render(state: TrackersPeriodState) { + graphHolder.onBind(state) + tabAdapter.updateDataSet(state) + updatePagerHeight() + } + + override fun onDestroyView() { + super.onDestroyView() + kotlin.runCatching { + if (oldPosition >= 0) { + val oldItem = binding.listsPager.findViewHolderForAdapterPosition(oldPosition) + oldItem?.itemView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) + } + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..0593e529741697ebb0568215bc7ae4f1e454723d --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt @@ -0,0 +1,105 @@ +/* + * 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.net.Uri +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.TrackersAndAppsListsUseCase +import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase +import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class TrackersPeriodViewModel( + private val period: Period, + private val trackersStatisticsUseCase: TrackersStatisticsUseCase, + private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase +) : ViewModel() { + + private val _state = MutableStateFlow( + TrackersPeriodState( + title = when (period) { + Period.DAY -> R.string.trackers_graph_title_day + Period.MONTH -> R.string.trackers_graph_title_month + Period.YEAR -> R.string.trackers_graph_title_year + }, + ) + ) + + val state = _state.asStateFlow() + + private val _singleEvents = MutableSharedFlow() + val singleEvents = _singleEvents.asSharedFlow() + + private val _navigate = MutableSharedFlow() + val navigate = _navigate.asSharedFlow() + + suspend fun doOnStartedState() = withContext(Dispatchers.IO) { + trackersStatisticsUseCase.listenUpdates().collect { + trackersStatisticsUseCase.getGraphData(period).let { graphData -> + _state.update { + it.copy( + callsBlockedNLeaked = graphData.callsBlockedNLeaked, + periods = graphData.periods, + trackersCount = graphData.trackersCount, + trackersAllowedCount = graphData.trackersAllowedCount, + graduations = graphData.graduations + ) + } + } + + trackersAndAppsListsUseCase.getAppsAndTrackersCounts(period).let { lists -> + _state.update { + it.copy( + trackers = lists.trackers, + allApps = lists.allApps, + appsWithTrackers = lists.appsWithTrackers + ) + } + } + } + } + + fun onClickTracker(tracker: Tracker) = viewModelScope.launch { + _navigate.emit(TrackersFragmentDirections.gotoTrackerDetailsFragment(trackerId = tracker.id)) + } + + fun onClickApp(app: ApplicationDescription) = viewModelScope.launch { + _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = app.uid)) + } + + fun onToggleHideNoTrackersApps() = viewModelScope.launch { + _state.update { it.copy(hideNoTrackersApps = !it.hideNoTrackersApps) } + } + + 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 1685c081fe65f8ab45af0954d54599f7001e96d0..f0787fd8509853a41ef7b74e242334e220cb81e9 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 @@ -18,19 +18,34 @@ 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.domain.entities.TrackersPeriodicStatistics import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import java.time.Instant +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.time.temporal.TemporalUnit -data class TrackersState( - val dayStatistics: TrackersPeriodicStatistics? = null, - val monthStatistics: TrackersPeriodicStatistics? = null, - val yearStatistics: TrackersPeriodicStatistics? = null, +data class TrackersPeriodState( + @StringRes val tabLabel: Int = R.string.empty, + @StringRes val title: Int = R.string.empty, + val callsBlockedNLeaked: List> = emptyList(), + val periods: List = emptyList(), + 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 hideNoTrackersApps: Boolean = false -) +) { + + fun isEmptyCalls(): Boolean { + return callsBlockedNLeaked.isEmpty() || + callsBlockedNLeaked.all { it.first == 0 && it.second == 0 } + } +} data class AppWithTrackersCount( val app: ApplicationDescription, @@ -41,3 +56,22 @@ data class TrackerWithAppsCount( val tracker: Tracker, val appsCount: Int = 0 ) + +enum class Period(val periodsCount: Int, val periodUnit: TemporalUnit) { + DAY(24, ChronoUnit.HOURS), + MONTH(30, ChronoUnit.DAYS), + YEAR(12, ChronoUnit.MONTHS); + + fun getPeriodStart(): Instant { + var start = ZonedDateTime.now() + .minus(periodsCount.toLong(), periodUnit) + .plus(1, periodUnit) + var truncatePeriodUnit = periodUnit + if (periodUnit === ChronoUnit.MONTHS) { + start = start.withDayOfMonth(1) + truncatePeriodUnit = ChronoUnit.DAYS + } + + return start.truncatedTo(truncatePeriodUnit).toInstant() + } +} 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 77da291fb0148bf7a5abb8dcfaae9a8b9555dfd2..d9b7b8b7346396b160c5f07b7999af2783672b61 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt @@ -22,74 +22,55 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription -import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase -import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase -import foundation.e.advancedprivacy.trackers.domain.entities.Tracker +import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.domain.usecases.TrackersScreenUseCase import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class TrackersViewModel( - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase -) : ViewModel() { - - private val _state = MutableStateFlow(TrackersState()) - val state = _state.asStateFlow() +class TrackersViewModel(private val trackersScreenUseCase: TrackersScreenUseCase) : ViewModel() { private val _singleEvents = MutableSharedFlow() val singleEvents = _singleEvents.asSharedFlow() private val _navigate = MutableSharedFlow() val navigate = _navigate.asSharedFlow() - suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - trackersStatisticsUseCase.listenUpdates().collect { - trackersStatisticsUseCase.getDayMonthYearStatistics() - .let { (day, month, year) -> - _state.update { s -> - s.copy( - dayStatistics = day, - monthStatistics = month, - yearStatistics = year - ) - } - } + val positionsCount = 3 + fun getPeriod(position: Int): Period { + return when (position) { + 0 -> Period.DAY + 1 -> Period.MONTH + 2 -> Period.YEAR + else -> Period.DAY + } + } - trackersAndAppsListsUseCase.getTrackersAndAppsLists().let { lists -> - _state.update { - it.copy( - trackers = lists.trackers, - allApps = lists.allApps, - appsWithTrackers = lists.appsWithTrackers - ) - } - } + fun getDisplayDuration(position: Int): Int { + return when (position) { + 0 -> R.string.trackers_period_day + 1 -> R.string.trackers_period_month + else -> R.string.trackers_period_year } } - fun onClickTracker(tracker: Tracker) = viewModelScope.launch { - _navigate.emit(TrackersFragmentDirections.gotoTrackerDetailsFragment(trackerId = tracker.id)) + suspend fun getLastPosition(): Int { + val lastPosition = trackersScreenUseCase.getLastPosition() + return if (lastPosition in 0 until positionsCount) { + lastPosition + } else { + 0 + } } - fun onClickApp(app: ApplicationDescription) = viewModelScope.launch { - _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appUid = app.uid)) + fun onDisplayedItemChanged(position: Int) = viewModelScope.launch(Dispatchers.IO) { + trackersScreenUseCase.savePosition(position) } fun onClickLearnMore() = viewModelScope.launch { _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS))) } - fun onToggleHideNoTrackersApps() = viewModelScope.launch { - _state.update { it.copy(hideNoTrackersApps = !it.hideNoTrackersApps) } - } - 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/graph/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/GraphHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..d4483c025c96a3c654c1f2097776993e8d9292b5 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/GraphHolder.kt @@ -0,0 +1,294 @@ +/* + * 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.graph + +import android.graphics.Canvas +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.github.mikephil.charting.components.AxisBase +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.components.YAxis.AxisDependency +import com.github.mikephil.charting.data.BarData +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.BarEntry +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.formatter.ValueFormatter +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.listener.OnChartValueSelectedListener +import com.github.mikephil.charting.renderer.XAxisRenderer +import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.common.extensions.dpToPxF +import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding +import foundation.e.advancedprivacy.features.trackers.TrackersPeriodState +import kotlin.math.floor + +class GraphHolder(private val binding: TrackersItemGraphBinding) { + + companion object { + private const val x_axis_graduations_count_threshold = 24 + } + + private val context = binding.root.context + private val barChart = binding.graph + + private val periodMarker = PeriodMarkerView(context) + + private var data = emptyList>() + private var labels = emptyList() + private var graduations: List = emptyList() + private val isHalfGraduations: Boolean + get() = graduations.size > x_axis_graduations_count_threshold + + private var isHighlighted = false + + private val onChartValueSelectedListener = object : OnChartValueSelectedListener { + override fun onValueSelected(e: Entry?, h: Highlight?) { + h?.let { + val index = it.x.toInt() + if (index >= 0 && + index < labels.size && + index < data.size + ) { + val period = labels[index] + val (blocked, leaked) = data[index] + periodMarker.setLabel(period, blocked, leaked) + } + } + isHighlighted = true + } + + override fun onNothingSelected() { + isHighlighted = false + } + } + + private val xAxisRenderer = object : XAxisRenderer( + barChart.viewPortHandler, barChart.xAxis, barChart.getTransformer(AxisDependency.LEFT) + ) { + override fun renderAxisLine(c: Canvas) { + mAxisLinePaint.color = mXAxis.axisLineColor + mAxisLinePaint.strokeWidth = mXAxis.axisLineWidth + mAxisLinePaint.pathEffect = mXAxis.axisLineDashPathEffect + + // Bottom line + c.drawLine( + mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom() - 5.dpToPxF(context), + + mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom() - 5.dpToPxF(context), + mAxisLinePaint + ) + } + + override fun renderGridLines(c: Canvas) { + if (!mXAxis.isDrawGridLinesEnabled || !mXAxis.isEnabled) return + val clipRestoreCount = c.save() + c.clipRect(gridClippingRect) + if (mRenderGridLinesBuffer.size != mAxis.mEntryCount * 2) { + mRenderGridLinesBuffer = FloatArray(mXAxis.mEntryCount * 2) + } + + val positions = mRenderGridLinesBuffer + mXAxis.mEntries.forEachIndexed { index, value -> + if ((index * 2 + 1) < positions.size) { + positions[index * 2] = value + positions[index * 2 + 1] = value + } + } + + mTrans.pointValuesToPixel(positions) + + val graduationPositions = positions.filterIndexed { index, _ -> index % 2 == 0 } + + setupGridPaint() + val gridLinePath = mRenderGridLinesPath + gridLinePath.reset() + + graduationPositions.forEachIndexed { i, x -> + + val graduationIndex = if (isHalfGraduations) 2 * i else i + val hasLabel = graduations.getOrNull(graduationIndex) != null + val bottomY = if (hasLabel) 0 else 3 + + gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 5.dpToPxF(context)) + gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context)) + + c.drawPath(gridLinePath, mGridPaint) + + gridLinePath.reset() + } + c.restoreToCount(clipRestoreCount) + } + } + + init { + with(barChart) { + description = null + setTouchEnabled(true) + setScaleEnabled(false) + + setDrawGridBackground(false) + setDrawBorders(false) + axisLeft.isEnabled = false + axisRight.isEnabled = false + + legend.isEnabled = false + + extraTopOffset = 40f + extraBottomOffset = 4f + + extraLeftOffset = 16f + extraRightOffset = 16f + + offsetTopAndBottom(0) + + minOffset = 0f + + offsetTopAndBottom(0) + + setDrawValueAboveBar(false) + + periodMarker.chartView = barChart + + marker = periodMarker + + setOnChartValueSelectedListener(onChartValueSelectedListener) + + setXAxisRenderer(xAxisRenderer) + } + } + + fun onBind(state: TrackersPeriodState) { + with(binding) { + title.text = context.getString(state.title) + val views = listOf( + helperText, + legendBlockedIcon, legendBlocked, + legendAllowedIcon, legendAllowed, + trackersDetected, trackersAllowed + ) + + if (state.isEmptyCalls()) { + graph.visibility = View.INVISIBLE + graphEmpty.isVisible = true + views.forEach { it.isVisible = false } + } else { + graph.isVisible = true + graphEmpty.isVisible = false + views.forEach { it.isVisible = true } + trackersDetected.text = context.getString( + R.string.trackers_graph_detected_trackers, + state.trackersCount + ) + trackersAllowed.text = context.getString( + R.string.trackers_graph_allowed_trackers, + state.trackersAllowedCount + ) + + refreshDataSet(state) + } + } + } + + private fun refreshDataSet(state: TrackersPeriodState) { + data = state.callsBlockedNLeaked + labels = state.periods + graduations = state.graduations ?: emptyList() + + val trackersDataSet = BarDataSet( + data.mapIndexed { index, value -> + BarEntry( + index.toFloat(), + floatArrayOf(value.first.toFloat(), value.second.toFloat()) + ) + }, + "" + ) + + val blockedColor = ContextCompat.getColor(context, R.color.switch_track_on) + val leakedColor = ContextCompat.getColor(context, R.color.red_off) + + trackersDataSet.colors = listOf( + blockedColor, + leakedColor + ) + trackersDataSet.setDrawValues(false) + + barChart.data = BarData(trackersDataSet) + prepareYAxis() + prepareXAxis() + + barChart.invalidate() + } + + private fun prepareYAxis() { + val maxValue = data.maxOfOrNull { it.first + it.second } ?: 0 + + barChart.axisLeft.apply { + isEnabled = true + + setDrawGridLines(false) + setDrawLabels(true) + setCenterAxisLabels(false) + setLabelCount(2, true) + textColor = context.getColor(R.color.primary_text) + valueFormatter = object : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + return if (value >= maxValue.toFloat()) maxValue.toString() else "" + } + } + } + } + + private val xAxisValueFormatter = object : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + val index = floor(value).toInt() + 1 + return graduations.getOrNull(index) ?: "" + } + } + + private val halfGraduationsXAxisValueFormatter = object : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + val index = floor(value).toInt() + 1 + return graduations.getOrNull(index) ?: graduations.getOrNull(index + 1) ?: "" + } + } + + private fun prepareXAxis() { + barChart.xAxis.apply { + isEnabled = true + position = XAxis.XAxisPosition.BOTTOM + + setDrawGridLines(true) + setDrawLabels(true) + setCenterAxisLabels(false) + textColor = context.getColor(R.color.primary_text) + + // setLabelCount can't have more than 25 labels. + if (isHalfGraduations) { + setLabelCount((graduations.size / 2) + 1, true) + valueFormatter = halfGraduationsXAxisValueFormatter + } else { + setLabelCount(graduations.size + 1, true) + valueFormatter = xAxisValueFormatter + } + } + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/PeriodMarkerView.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/PeriodMarkerView.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd7730cbab7986e6e5030bc110a59576d11aaa19 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/PeriodMarkerView.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.features.trackers.graph + +import android.content.Context +import android.graphics.Canvas +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.DynamicDrawableSpan +import android.text.style.ImageSpan +import android.view.View +import android.widget.TextView +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import com.github.mikephil.charting.components.MarkerView +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.utils.MPPointF +import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.common.extensions.dpToPxF + +class PeriodMarkerView(context: Context) : MarkerView(context, R.layout.chart_tooltip_2) { + enum class ArrowPosition { LEFT, CENTER, RIGHT } + + private val arrowMargins = 10.dpToPxF(context) + private val mOffset2 = MPPointF(0f, 0f) + + private fun getArrowPosition(posX: Float): ArrowPosition { + val halfWidth = width / 2 + + return chartView?.let { chart -> + if (posX < halfWidth) { + ArrowPosition.LEFT + } else if (chart.width - posX < halfWidth) { + ArrowPosition.RIGHT + } else { + ArrowPosition.CENTER + } + } ?: ArrowPosition.CENTER + } + + private fun showArrow(position: ArrowPosition?) { + val ids = listOf( + R.id.arrow_top_left, R.id.arrow_top_center, R.id.arrow_top_right, + R.id.arrow_bottom_left, R.id.arrow_bottom_center, R.id.arrow_bottom_right + ) + + val toShow = when (position) { + ArrowPosition.LEFT -> R.id.arrow_bottom_left + ArrowPosition.CENTER -> R.id.arrow_bottom_center + ArrowPosition.RIGHT -> R.id.arrow_bottom_right + else -> null + } + + ids.forEach { id -> + val showIt = id == toShow + findViewById(id)?.let { + if (it.isVisible != showIt) { + it.isVisible = showIt + } + } + } + } + + fun setLabel(period: String, blocked: Int, leaked: Int) { + val span = SpannableStringBuilder(period) + span.append(" | ") + span.setSpan( + ImageSpan(context, R.drawable.ic_legend_blocked_2, DynamicDrawableSpan.ALIGN_BASELINE), + span.length - 1, + span.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + span.append(" $blocked ") + span.setSpan( + ImageSpan(context, R.drawable.ic_legend_leaked, DynamicDrawableSpan.ALIGN_BASELINE), + span.length - 1, + span.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + span.append(" $leaked") + findViewById(R.id.label).text = span.toSpannable() + } + + override fun refreshContent(e: Entry?, highlight: Highlight?) { + highlight?.let { + showArrow(getArrowPosition(highlight.xPx)) + } + super.refreshContent(e, highlight) + } + + override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF { + val x = when (getArrowPosition(posX)) { + ArrowPosition.LEFT -> -arrowMargins + ArrowPosition.RIGHT -> -width + arrowMargins + ArrowPosition.CENTER -> -width.toFloat() / 2 + } + + mOffset2.x = x + mOffset2.y = -posY + + return mOffset2 + } + + override fun draw(canvas: Canvas?, posX: Float, posY: Float) { + super.draw(canvas, posX, posY) + } +} diff --git a/app/src/main/res/drawable/bg_rounded_19.xml b/app/src/main/res/drawable/bg_rounded_19.xml new file mode 100644 index 0000000000000000000000000000000000000000..fb2a784146d77fc09c2e4d47e37bbc4d818f3f60 --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_19.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/bg_tag.xml b/app/src/main/res/drawable/bg_tag.xml new file mode 100644 index 0000000000000000000000000000000000000000..5243140ace320c6875946e4f8e4d7706712a9b17 --- /dev/null +++ b/app/src/main/res/drawable/bg_tag.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_legend_blocked_2.xml b/app/src/main/res/drawable/ic_legend_blocked_2.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a146a8bd85b2641ab5b732cb7399a38f90fce01 --- /dev/null +++ b/app/src/main/res/drawable/ic_legend_blocked_2.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/part_triangle.xml b/app/src/main/res/drawable/part_triangle.xml new file mode 100644 index 0000000000000000000000000000000000000000..f835c4436b4d514f0e61d36506b94eba2a0f4a7b --- /dev/null +++ b/app/src/main/res/drawable/part_triangle.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/chart_tooltip_2.xml b/app/src/main/res/layout/chart_tooltip_2.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fabd50dd84e2fea7862bdec7d33500e06105866 --- /dev/null +++ b/app/src/main/res/layout/chart_tooltip_2.xml @@ -0,0 +1,66 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_trackers.xml b/app/src/main/res/layout/fragment_trackers.xml index 683f01c99e9d0633a6a5c980f36eec810e6ec83c..89f6d4ed913745b57b416a70ef6dbca9e66ab34e 100644 --- a/app/src/main/res/layout/fragment_trackers.xml +++ b/app/src/main/res/layout/fragment_trackers.xml @@ -47,67 +47,27 @@ android:lineSpacingExtra="5sp" android:text="@string/trackers_info" /> - - - - - + app:tabTextAppearance="@style/TabsTextTheme" /> + android:layout_height="wrap_content" /> + diff --git a/app/src/main/res/layout/trackers_item_graph.xml b/app/src/main/res/layout/trackers_item_graph.xml index aabc10847e404318b9cde91e78bae0b7a03304ff..b21371d28a8f59e5842a7148ad18a5a783faed82 100644 --- a/app/src/main/res/layout/trackers_item_graph.xml +++ b/app/src/main/res/layout/trackers_item_graph.xml @@ -1,4 +1,5 @@ - - - - - + android:layout_marginLeft="16dp" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Leaks in the last 24 hours" + android:textSize="14sp" + android:lineHeight="20dp" + android:textColor="@color/primary_text" + android:textFontWeight="400" + /> - + - + - + + - - + + - - + - - - \ No newline at end of file + + + + + diff --git a/app/src/main/res/layout/trackers_period_fragment.xml b/app/src/main/res/layout/trackers_period_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..93647e61a78907c23a083b11e5cc8b3cd2679208 --- /dev/null +++ b/app/src/main/res/layout/trackers_period_fragment.xml @@ -0,0 +1,61 @@ + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3277840ccc9abcb0a4078d3977955fa4ffa16422..60e312ae1a8a2c373d621fcc048486eec831d176 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -48,13 +48,15 @@ Wende diese Einstellungen auf alle ausgewählten Anwendungen an * : System Tracker (Verfolger) sind kleine Programme, die in Anwendungen eingebaut werden. Sie sammeln deine Daten und verfolgen andauernd deine Aktivität. Du kannst sehen, wie viele Tracker aktiv sind, und kannst sie alle für den bestmöglichen Schutz blockieren. Das kann aber auch manche Apps am korrekten Funktionieren hindern, darum kannst du selbst auswählen, welche Tracker du blockieren möchtest. - %d Tracker 24 Stunden vergangener Monat vergangenes Jahr HH:mm d\'.\' MMMM EEE MMMM yyyy + dd/MM + MMM + Tracker-Kontrolle anschalten Es wurden noch keine Tracker gefunden. Sobald welche entdeckt werden, wird das hier aktualisiert. Es wurden noch keine Tracker entdeckt. Alle zukünftigen Tracker werden blockiert. diff --git a/app/src/main/res/values-en-rUS/strings.xml b/app/src/main/res/values-en-rUS/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..426be81bf42a8a2e2b4290ef8c06bbea0f603091 --- /dev/null +++ b/app/src/main/res/values-en-rUS/strings.xml @@ -0,0 +1,9 @@ + + + HH:mm + MMMM d - EEE + MMMM yyyy + + MM/dd + MMM + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5b8a9764dc580d4187f7da21e824822484f1593f..787e49d65f6e0aca2e4810a3938de2db9f4c541f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -44,10 +44,12 @@ HH:mm EEE d \'de\' MMMM MMMM yyyy + dd/MM + MMM + Rastreadores bloqueados No se ha detectado ningún rastreador. Nuevos rastreadores se actualizarán aquí. Privacidad rápida activada para utilizar funciones - %d rastreadores No se ha detectado ningún rastreador. Futuros rastreadores serán todos bloqueados. Tu privacidad en línea está protegida %s rastreadores te han perfilado en las últimas 24 horas diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index ec47879e921c5cc2a3a8b6ac745f62d046f77bd2..f9a16c065c28629bd9c0137b0e245fb59e55e3ff 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -15,10 +15,11 @@ MMMM yyyy d MMMM EEE HH:mm + dd/MM + MMM kulunut vuosi kulunut kuukausi 24 tuntia - %d seurainta Seuraimet ovat sovelluksiin piilotettuja koodinpätkiä. Ne keräävät tietojasi ja seuraavat toimintaasi 24/7. Katso mitkä seuraimet ovat aktiivisia ja estä ne kaikki, parhaan mahdollisen suojan saavuttamiseksi. Koska tämä voi aiheuttaa joidenkin sovellusten osalta toimintahäiriöitä, voit valita erikseen mitä seuraimia haluat estää. Hallitse sovellusten seuraimia Virheelliset koordinaatit diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8beb582ea9815b61d1abd2af1358cfc922c6f750..7f34c62cac9cc2214270ef333514b8543d13a2bc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -40,11 +40,7 @@ 24 heures mois précédent année précédente - HH:mm - EEE d MMMM - MMMM yyyy Gérer les pisteurs - %d pisteurs Activer le contrôle des pisteurs Aucun pisteur n\'a été détecté pour l\'instant. Si de nouveaux pisteurs sont détectés, ils seront listés ici. Aucun pisteur n\'a été détecté pour l\'instant. Tous les futurs pisteurs seront bloqués. @@ -136,4 +132,10 @@ Apps avec pisteurs Pisteurs Bloqués + + HH:mm + EEE d MMMM + MMMM yyyy + dd/MM + MMM \ No newline at end of file diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 47095c5a5f87bf17aad5799136048b098af51c0f..14893dae8695f8e76d9cb82d0af24dfde22aab3b 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -24,13 +24,15 @@ Breiddargráða Hnitin eru ógild Kanna nánar - %d rekjarar 24 klukkustundir síðasta mánuðinn síðasta árið HH:mm d. MMMM, EEE MMMM yyyy + dd/MM + MMM + Víxla á rekjarastýringu Forritið er ekki uppsett. Ekki sýna þetta aftur diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4be8319aff77458868624f3f0c01fbb550312ce7..1842dd53395d20921ccc97cd6b2341c39db24b53 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -39,13 +39,15 @@ Latitudine Coordinate non valide Gestisci i tracker delle app - %d tracker 24 ore mese scorso scorso anno HH:mm - MMMM d - EEE + EEE d MMMM MMMM yyyy + dd/MM + MMM + Blocca tracker Non sono ancora stati rilevati tracker. Nel caso in cui accadesse verranno mostrati qui. Non sono ancora stati rilevati tracker. Tutti quelli trovati in futuro verranno bloccati. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 114360418ba7b18b343a4bfde003762fb20948a4..d96d54c034b265f3b12ff193e0b26cccbe0392e3 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -7,4 +7,11 @@ Geblokkeerde lekken Gefeliciteerd! Er zijn geen trackers you aan het profileren. Systeem + + HH:mm + EEE d MMMM + MMMM yyyy + dd/MM + MMM + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index af7cdadb6458a533177b47179736dc50873499a1..2793ec627ff69190c6816b4e3c3527f051316752 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -55,10 +55,12 @@ MMMM yyyy d MMMM EEE HH:mm + dd/MM + MMM + прошлый год прошедший месяц 24 часа - %d трекеры Узнать больше Трекеры - это фрагменты кода, скрытые в приложениях. Они собирают ваши данные и следят за вашей активностью 24 часа в сутки 7 дней в неделю. Узнайте, какие трекеры активны, и заблокируйте их все для наилучшей защиты. Поскольку это может привести к сбоям в работе некоторых приложений, вы можете выбрать, какие именно трекеры вы хотите заблокировать. Управление трекерами приложений diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4ddc7b31633532cfe6b52fbf2f4819cd7973c475..b5b70984ba1bc1aec7bf947e94fa5ffef95c7206 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -66,7 +66,6 @@ Hantera appars spårare Spårare är bitar av kod gömda i appar. Dom samlar in din data och följer din aktivitet dygnet runt. Se vilka spårare som är aktiva och blockera alla för bästa skydd. Eftersom det kan få vissa applikationer att inte fungera kan du specifikt välja vilka spårare som du vill blockera. Lär dig mer - %d spårare 24 timmar senaste månaden senaste året @@ -90,8 +89,11 @@ Inaktivera tredjeparts-VPN %s för att dölja din verkliga IP-adress med Avancerad Integritet. Vår tjänst för förvrängning av IP tar tid att starta. Det kan ta några minuter. Att lämna sidan kommer inte avbryta processen. HH:mm - MMMM d - EEE + EEE d MMMM MMMM yyyy + dd/MM + MMM + Aktivera Snabb integritet för att kunna aktivera/inaktivera spårare. Spårarkontroll Medan detta alternativ är aktiverat kan, i sällsynta fall, vissa appar sluta fungera korrekt. Om du stöter på problem kan du inaktivera Spårarkontroll för specifika appar och webbsidor när som helst. diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e0e9530b64e5754a02d468576b1c57a0f31660f9..2e2b73a88dd46cf0367571d86a1ce24fb4df82fa 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -31,11 +31,16 @@ @color/e_divider_color @color/e_background_overlay + + @color/e_switch_track_on + @color/e_palette_9 + + #32F8432E #263238 #FFFFFFFF #28C97C - #F8432E + #AADCFE diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..8cb46efc35ae1b1a048b2909f78d1aa3cf1872ea --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 32dp + 16dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ea109467514345fc45a36fb0f6e727984da8a8e..3df052cb5864de8639022b9a69d50cae7c85648e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Advanced Privacy + Close OK @@ -91,13 +92,20 @@ Manage apps\' trackers - Trackers are pieces of code hidden in apps. They collect your data and follow your activity 24/7. See which trackers are active and block them all for best protection. As it could cause some applications to malfunction, you can choose specifically which trackers you want to block. - Learn more - %d trackers - 24 hours - past month - past year - @string/ipscrambling_app_list_infos + Trackers are pieces of code hidden in apps. They collect your data and follow your activity 24/7. See below all app tracking activity in your device. + Know more + + Day + Month + Year + Leaks in the last 24 hours + Leaks in the last 30 days + Leaks in the last 12 months + Tap for more info + Blocked leaks + Allowed leaks + %d detected trackers + %d allowed Apps Trackers @@ -107,9 +115,12 @@ %s trackers detected detected in %s apps - HH:mm - MMMM d - EEE - MMMM yyyy + HH:mm + EEE d MMMM + MMMM yyyy + + dd/MM + MMM %s tracking summary diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d60d1dc2c6fed1d306f8389a2bf18032568daeed..7c1e4c385db3c27da591aa05018e30be3e6e7b2c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -26,4 +26,9 @@ @null + + \ No newline at end of file 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 0266f850b3ceaf9fc7d48c19a323e3f98fca1716..86e1acf94c9616a4f7f25d80a655d27b9fab7924 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 @@ -56,4 +56,6 @@ interface LocalStateRepository { var hideWarningLocation: Boolean var hideWarningIpScrambling: Boolean + + var trackersScreenLastPosition: 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 a80d4dc09a7f51c38b116dea42f0b2aeb07f25ce..dc726db38f0caf7bf03368cd0b277d02ffddc324 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 @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.withContext import timber.log.Timber +import java.time.Instant import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoUnit @@ -206,6 +207,53 @@ class StatsDatabase( } } + suspend fun getTrackersCount(periodsCount: Int, periodUnit: TemporalUnit): Int = withContext(Dispatchers.IO) { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodsCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + minTimestamp) + val projection = + "COUNT(DISTINCT $COLUMN_NAME_TRACKER) $PROJECTION_NAME_TRACKERS_COUNT" + + val cursor = db.rawQuery( + "SELECT $projection FROM $TABLE_NAME WHERE $selection", + selectionArg + ) + var count = 0 + if (cursor.moveToNext()) { + count = cursor.getInt(0) + } + cursor.close() + db.close() + count + } + } + + suspend fun getLeakedTrackersCount(periodsCount: Int, periodUnit: TemporalUnit): Int = withContext(Dispatchers.IO) { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodsCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ? AND " + + "$COLUMN_NAME_NUMBER_CONTACTED > $COLUMN_NAME_NUMBER_BLOCKED" + val selectionArg = arrayOf("" + minTimestamp) + val projection = + "COUNT(DISTINCT $COLUMN_NAME_TRACKER) $PROJECTION_NAME_TRACKERS_COUNT" + + val cursor = db.rawQuery( + "SELECT $projection FROM $TABLE_NAME WHERE $selection", + selectionArg + ) + var count = 0 + if (cursor.moveToNext()) { + count = cursor.getInt(0) + } + cursor.close() + db.close() + count + } + } + fun getContactedTrackersCount(): Int { synchronized(lock) { val db = readableDatabase @@ -275,13 +323,23 @@ class StatsDatabase( } } - fun getDistinctTrackerAndApp(): List> { + fun getDistinctTrackerAndApp(periodStart: Instant): List> { synchronized(lock) { val db = readableDatabase - val projection = "$COLUMN_NAME_APPID, $COLUMN_NAME_TRACKER" - val cursor = db.rawQuery( - "SELECT DISTINCT $projection FROM $TABLE_NAME", // + - arrayOf() + val projection = arrayOf(COLUMN_NAME_APPID, COLUMN_NAME_TRACKER) + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + periodStart.epochSecond) + val cursor = db.query( + true, + TABLE_NAME, + projection, + selection, + selectionArg, + null, + null, + null, + null + ) val res = mutableListOf>()