From 9b6700a6835f113d40c99550caf631665038772e Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 2 Nov 2023 00:23:11 +0530 Subject: [PATCH 1/8] 1465: date selector for graph UI by SayantanRC --- .../features/trackers/TrackersFragment.kt | 47 +++++++++++---- .../features/trackers/TrackersGraphAdapter.kt | 60 +++++++++++++++++++ app/src/main/res/layout/fragment_trackers.xml | 45 ++++++-------- app/src/main/res/values-night/colors.xml | 2 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/dimens.xml | 5 ++ app/src/main/res/values/strings.xml | 10 ++-- app/src/main/res/values/themes.xml | 5 ++ 8 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt create mode 100644 app/src/main/res/values/dimens.xml 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 b88c55ea..d74002dd 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 @@ -18,6 +18,7 @@ package foundation.e.advancedprivacy.features.trackers +import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle @@ -33,10 +34,12 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.GraphHolder @@ -54,9 +57,7 @@ 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 var adapter: TrackersGraphAdapter? = null private lateinit var tabAdapter: ListsTabPagerAdapter @@ -65,9 +66,8 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { 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) + val trackersTabs = binding.trackersDurationTabs + val trackersPager = binding.trackersDurationPager tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel) binding.listsPager.adapter = tabAdapter @@ -90,6 +90,22 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } }) + adapter = TrackersGraphAdapter(requireContext(), ::renderGraph) + trackersPager.adapter = adapter + TabLayoutMediator(trackersTabs, trackersPager) { tab, position -> + tab.text = getDisplayDuration(position) + }.attach() + + trackersTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + //tabSelection.postValue(tab?.position ?: -1) + } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + + override fun onTabReselected(tab: TabLayout.Tab?) {} + }) + setupTrackersInfos() listenViewModel() @@ -143,6 +159,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } } + private fun setupTrackersInfos() { val infoText = getString(R.string.trackers_info) val moreText = getString(R.string.trackers_info_more) @@ -203,15 +220,24 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } + @SuppressLint("NotifyDataSetChanged") 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) } + adapter?.setState(state) + adapter?.notifyDataSetChanged() updatePagerHeight() tabAdapter.updateDataSet(state) } + private fun getDisplayDuration(position: Int): String { + return when (position) { + 0 -> getString(R.string.trackers_period_day) + 1 -> getString(R.string.trackers_period_month) + 2 -> getString(R.string.trackers_period_year) + else -> "" + } + } + private fun renderGraph( statistics: TrackersPeriodicStatistics, graphHolder: GraphHolder, @@ -238,8 +264,5 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { oldItem?.itemView?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener) } } - dayGraphHolder = null - monthGraphHolder = null - yearGraphHolder = null } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt new file mode 100644 index 00000000..137f73ff --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt @@ -0,0 +1,60 @@ +package foundation.e.advancedprivacy.features.trackers + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import foundation.e.advancedprivacy.common.GraphHolder +import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding +import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics + + +class TrackersGraphAdapter( + private val context: Context, + private val render: ( + statistics: TrackersPeriodicStatistics, + graphHolder: GraphHolder, + graphBinding: TrackersItemGraphBinding + ) -> Unit, +) : RecyclerView.Adapter() { + + private var state: TrackersState? = null + + fun setState(state: TrackersState) { + this.state = state + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphViewHolder { + val binding = TrackersItemGraphBinding.inflate(LayoutInflater.from(context), parent, false) + return GraphViewHolder(binding, binding.root) + } + + override fun getItemCount(): Int { + return 3 + } + + override fun onBindViewHolder(holder: GraphViewHolder, position: Int) { + val graphHolder = GraphHolder(holder.binding.graph, context, false) + val statistics = when (position) { + 0 -> state?.dayStatistics + 1 -> state?.monthStatistics + 2 -> state?.yearStatistics + else -> null + } + statistics?.let { render(it, graphHolder, holder.binding) } + } +} + +class GraphViewHolder( + val binding: TrackersItemGraphBinding, + view: View, +) : RecyclerView.ViewHolder(view) { + init { + view.layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } +} \ 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 683f01c9..07a3894a 100644 --- a/app/src/main/res/layout/fragment_trackers.xml +++ b/app/src/main/res/layout/fragment_trackers.xml @@ -47,36 +47,27 @@ android:lineSpacingExtra="5sp" android:text="@string/trackers_info" /> - - - - + + + android:layout_height="wrap_content" /> + #169659 + + #212121 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e0e9530b..54da383b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -50,4 +50,6 @@ @color/e_secondary_text_color_light @color/e_disabled_color_light + + #E0E0E0 \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..8cb46efc --- /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 0ea10946..08ea0b5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,12 +91,12 @@ 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 + 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 %d trackers - 24 hours - past month - past year + Day + Month + Year @string/ipscrambling_app_list_infos Apps diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d60d1dc2..7c1e4c38 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 -- GitLab From e9f9bac1170e0b7b8e29181612387a4141a04dc1 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Sun, 26 Nov 2023 17:39:18 +0100 Subject: [PATCH 2/8] 1465: Update graph UI. --- app/build.gradle | 13 +- .../entities/TrackersPeriodicStatistics.kt | 5 + .../usecases/TrackersStatisticsUseCase.kt | 57 ++- .../features/trackers/GraphHolder.kt | 367 ++++++++++++++++++ .../features/trackers/TrackersFragment.kt | 60 ++- .../features/trackers/TrackersGraphAdapter.kt | 45 +-- .../features/trackers/TrackersViewModel.kt | 16 +- app/src/main/res/drawable/bg_rounded_19.xml | 23 ++ app/src/main/res/drawable/bg_tag.xml | 6 + .../main/res/drawable/ic_legend_blocked_2.xml | 21 + app/src/main/res/drawable/part_triangle.xml | 41 ++ app/src/main/res/layout/chart_tooltip_2.xml | 65 ++++ app/src/main/res/layout/fragment_trackers.xml | 2 +- .../main/res/layout/trackers_item_graph.xml | 239 +++++++----- app/src/main/res/values/colors.xml | 7 +- app/src/main/res/values/strings.xml | 15 +- build.gradle | 2 +- 17 files changed, 806 insertions(+), 178 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt create mode 100644 app/src/main/res/drawable/bg_rounded_19.xml create mode 100644 app/src/main/res/drawable/bg_tag.xml create mode 100644 app/src/main/res/drawable/ic_legend_blocked_2.xml create mode 100644 app/src/main/res/drawable/part_triangle.xml create mode 100644 app/src/main/res/layout/chart_tooltip_2.xml diff --git a/app/build.gradle b/app/build.gradle index a10ead1c..1d056350 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/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt index c0fa637b..001527ba 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 @@ -17,7 +17,12 @@ package foundation.e.advancedprivacy.domain.entities +import androidx.annotation.StringRes + data class TrackersPeriodicStatistics( + // TODO: dont let O for stringres !! + @StringRes val tabLabel: Int = 0, + @StringRes val title: Int = 0, val callsBlockedNLeaked: List>, val periods: List, val trackersCount: Int, 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 8f290b85..ebaa460f 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 @@ -19,6 +19,7 @@ package foundation.e.advancedprivacy.domain.usecases import android.content.res.Resources +import android.util.Log import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.throttleFirst import foundation.e.advancedprivacy.data.repositories.AppListsRepository @@ -104,7 +105,52 @@ class TrackersStatisticsUseCase( periods.add(if (start.hour % 6 == 0) formatter.format(start) else null) end = start.minus(1, ChronoUnit.MINUTES) } - return periods.reversed() + return periods.reversed().let { + Log.d("DebugPeriod", "day periods: $it") + it + } + } + + private fun buildMonthGraduations(): List { + val formatter = DateTimeFormatter.ofPattern( + "MM/dd" + ) + + 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().let { + Log.d("DebugPeriod", "month periods: $it") + it + } + } + + private fun buildYearGraduations(): List { + val formatter = DateTimeFormatter.ofPattern( + "MMM" + // resources.getString(R.string.trackers_graph_hours_period_format) // TODO. + ) + + val periods = mutableListOf() + var end = ZonedDateTime.now() + for (i in 1..12) { + val start = end.truncatedTo(ChronoUnit.DAYS).let { + it.minusDays(it.dayOfMonth.toLong()).let { + Log.d("DebugPeriod", "start: $it") + it + } + } + periods.add(if (start.monthValue % 3 == 0) formatter.format(start) else null) + end = start.minus(1, ChronoUnit.DAYS) + } + return periods.reversed().let { + Log.d("DebugPeriod", "years periods: $it") + it + } } private fun buildDayLabels(): List { @@ -152,17 +198,20 @@ class TrackersStatisticsUseCase( TrackersPeriodicStatistics( callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS), periods = buildDayLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS) + trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS), + graduations = buildDayGraduations(), ), TrackersPeriodicStatistics( callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(30, ChronoUnit.DAYS), periods = buildMonthLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(30, ChronoUnit.DAYS) + trackersCount = statisticsUseCase.getActiveTrackersByPeriod(30, ChronoUnit.DAYS), + graduations = buildMonthGraduations(), ), TrackersPeriodicStatistics( callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS), periods = buildYearLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS) + trackersCount = statisticsUseCase.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS), + graduations = buildYearGraduations(), ) ) } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt new file mode 100644 index 00000000..ea244cbd --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.features.trackers + +import android.content.Context +import android.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.content.ContextCompat +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.github.mikephil.charting.components.AxisBase +import com.github.mikephil.charting.components.MarkerView +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 com.github.mikephil.charting.utils.MPPointF +import foundation.e.advancedprivacy.R +import foundation.e.advancedprivacy.common.extensions.dpToPxF +import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding +import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics +import kotlin.math.floor + +class GraphHolder(private val binding: TrackersItemGraphBinding) : RecyclerView.ViewHolder(binding.root) { + + private val context = itemView.context + private val barChart = binding.graph + + private var data = emptyList>() + private var labels = emptyList() + private var graduations: List? = null + + private var isHighlighted = false + + init { + barChart.description = null + barChart.setTouchEnabled(true) + barChart.setScaleEnabled(false) + + barChart.setDrawGridBackground(false) + barChart.setDrawBorders(false) + barChart.axisLeft.isEnabled = false + barChart.axisRight.isEnabled = false + + barChart.legend.isEnabled = false + + barChart.extraTopOffset = 40f + barChart.extraBottomOffset = 4f + + barChart.minOffset = 0f + + val periodMarker = PeriodMarkerView(context) + periodMarker.chartView = barChart + barChart.marker = periodMarker + + barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { + override fun onValueSelected(e: Entry?, h: Highlight?) { + h?.let { + val index = it.x.toInt() + if (index >= 0 && + index < labels.size && + index < this@GraphHolder.data.size + ) { + val period = labels[index] + val (blocked, leaked) = this@GraphHolder.data[index] + periodMarker.setLabel(period, blocked, leaked) + } + } + isHighlighted = true + } + + override fun onNothingSelected() { + isHighlighted = false + } + }) + } + + 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 fun prepareXAxisDashboardDay() { + barChart.offsetTopAndBottom(0) + + val graduationsCount = graduations?.size ?: 2 + + barChart.setXAxisRenderer(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 + run { + var i = 0 + while (i < positions.size) { + positions[i] = mXAxis.mEntries[i / 2] + positions[i + 1] = mXAxis.mEntries[i / 2] + i += 2 + } + } + + mTrans.pointValuesToPixel(positions) + setupGridPaint() + val gridLinePath = mRenderGridLinesPath + gridLinePath.reset() + var i = 0 + while (i < positions.size) { + + val bottomY = if (graduations?.getOrNull( + if (graduationsCount < 24) i / 2 else i + ) != null + ) 0 else 3 + val x = positions[i] + gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 5.dpToPxF(context)) + gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context)) + + c.drawPath(gridLinePath, mGridPaint) + + gridLinePath.reset() + + i += 2 + } + c.restoreToCount(clipRestoreCount) + } + }) + + barChart.setDrawValueAboveBar(false) + 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 (graduationsCount < 24) { + setLabelCount((graduations?.size ?: 24) + 1, true) + + valueFormatter = object : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + return (graduations?.getOrNull(floor(value).toInt() + 1) ?: "") + } + } + } else { + setLabelCount((graduationsCount / 2) + 1, true) + valueFormatter = 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 refreshDataSet() { + val trackersDataSet = BarDataSet( + data.mapIndexed { index, value -> + BarEntry( + index.toFloat(), + floatArrayOf(value.first.toFloat(), value.second.toFloat()) + ) + }, + "" + ).apply { + + val blockedColor = ContextCompat.getColor(context, R.color.switch_track_on) + val leakedColor = ContextCompat.getColor(context, R.color.red_off) + + colors = listOf( + blockedColor, + leakedColor + ) + + setDrawValues(false) + } + + barChart.data = BarData(trackersDataSet) + prepareYAxis() + prepareXAxisDashboardDay() + + barChart.invalidate() + } + + fun onBind(state: TrackersPeriodicStatistics) { + with(binding) { + title.text = itemView.context.getString(state.title) + val views = listOf( + helperText, + legendBlockedIcon, legendBlocked, + legendAllowedIcon, legendAllowed, + trackersDetected, trackersAllowed + ) + if (state.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) { + graph.visibility = View.INVISIBLE + graphEmpty.isVisible = true + views.forEach { it.isVisible = false } + } else { + graph.isVisible = true + graphEmpty.isVisible = false + views.forEach { it.isVisible = true } + data = state.callsBlockedNLeaked + labels = state.periods + graduations = state.graduations + refreshDataSet() + + trackersDetected.text = context.getString( + R.string.trackers_graph_detected_trackers, + state.trackersCount + ) + trackersAllowed.text = + context.getString(R.string.trackers_graph_allowed_trackers, -12) + } + } + } +} + +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/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt index d74002dd..74f0557f 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 @@ -32,9 +32,7 @@ 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.MutableLiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController @@ -42,13 +40,10 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout 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 @@ -57,7 +52,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private lateinit var binding: FragmentTrackersBinding - private var adapter: TrackersGraphAdapter? = null + private lateinit var graphsAdapter: TrackersGraphAdapter private lateinit var tabAdapter: ListsTabPagerAdapter @@ -66,9 +61,6 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { binding = FragmentTrackersBinding.bind(view) - val trackersTabs = binding.trackersDurationTabs - val trackersPager = binding.trackersDurationPager - tabAdapter = ListsTabPagerAdapter(requireContext(), viewModel) binding.listsPager.adapter = tabAdapter @@ -90,15 +82,19 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } }) - adapter = TrackersGraphAdapter(requireContext(), ::renderGraph) - trackersPager.adapter = adapter + val trackersTabs = binding.trackersDurationTabs + val trackersPager = binding.trackersDurationPager + + graphsAdapter = TrackersGraphAdapter(requireContext()) + trackersPager.adapter = graphsAdapter TabLayoutMediator(trackersTabs, trackersPager) { tab, position -> + // TODO tab.text = getDisplayDuration(position) }.attach() trackersTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { - //tabSelection.postValue(tab?.position ?: -1) + // tabSelection.postValue(tab?.position ?: -1) } override fun onTabUnselected(tab: TabLayout.Tab?) {} @@ -159,7 +155,6 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } } - private fun setupTrackersInfos() { val infoText = getString(R.string.trackers_info) val moreText = getString(R.string.trackers_info_more) @@ -222,11 +217,10 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { @SuppressLint("NotifyDataSetChanged") private fun render(state: TrackersState) { - adapter?.setState(state) - adapter?.notifyDataSetChanged() - updatePagerHeight() + graphsAdapter.setState(state) tabAdapter.updateDataSet(state) + updatePagerHeight() } private fun getDisplayDuration(position: Int): String { @@ -238,23 +232,23 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } } - 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) - } - } +// 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() diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt index 137f73ff..6f044dcf 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt @@ -1,60 +1,43 @@ package foundation.e.advancedprivacy.features.trackers import android.content.Context +import android.util.Log import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView -import foundation.e.advancedprivacy.common.GraphHolder import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding -import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics - class TrackersGraphAdapter( - private val context: Context, - private val render: ( - statistics: TrackersPeriodicStatistics, - graphHolder: GraphHolder, - graphBinding: TrackersItemGraphBinding - ) -> Unit, -) : RecyclerView.Adapter() { + private val context: Context +) : RecyclerView.Adapter() { private var state: TrackersState? = null fun setState(state: TrackersState) { this.state = state + notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphViewHolder { +// override fun getItemViewType(position: Int): Int { +// return position +// } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphHolder { + Log.d("DebugPeriod", "onCreateViewHolder viewType: $viewType") val binding = TrackersItemGraphBinding.inflate(LayoutInflater.from(context), parent, false) - return GraphViewHolder(binding, binding.root) + return GraphHolder(binding) } override fun getItemCount(): Int { return 3 } - override fun onBindViewHolder(holder: GraphViewHolder, position: Int) { - val graphHolder = GraphHolder(holder.binding.graph, context, false) - val statistics = when (position) { + override fun onBindViewHolder(holder: GraphHolder, position: Int) { + when (position) { 0 -> state?.dayStatistics 1 -> state?.monthStatistics 2 -> state?.yearStatistics else -> null - } - statistics?.let { render(it, graphHolder, holder.binding) } + }?.let { holder.onBind(it) } } } - -class GraphViewHolder( - val binding: TrackersItemGraphBinding, - view: View, -) : RecyclerView.ViewHolder(view) { - init { - view.layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - } -} \ No newline at end of file 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 77da291f..b159d538 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,6 +22,7 @@ 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 @@ -55,9 +56,18 @@ class TrackersViewModel( .let { (day, month, year) -> _state.update { s -> s.copy( - dayStatistics = day, - monthStatistics = month, - yearStatistics = year + dayStatistics = day.copy( + tabLabel = R.string.trackers_period_day, + title = R.string.trackers_graph_title_day, + ), + monthStatistics = month.copy( + tabLabel = R.string.trackers_period_month, + title = R.string.trackers_graph_title_month, + ), + yearStatistics = year.copy( + tabLabel = R.string.trackers_period_year, + title = R.string.trackers_graph_title_year, + ), ) } } 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 00000000..fb2a7841 --- /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 00000000..5243140a --- /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 00000000..d1ae0152 --- /dev/null +++ b/app/src/main/res/drawable/ic_legend_blocked_2.xml @@ -0,0 +1,21 @@ + + + + + + \ 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 00000000..f835c443 --- /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 00000000..0399f2e7 --- /dev/null +++ b/app/src/main/res/layout/chart_tooltip_2.xml @@ -0,0 +1,65 @@ + + + + + + + + + \ 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 07a3894a..eeefa891 100644 --- a/app/src/main/res/layout/fragment_trackers.xml +++ b/app/src/main/res/layout/fragment_trackers.xml @@ -72,7 +72,7 @@ android:id="@+id/trackers_lists_title" android:layout_height="wrap_content" android:layout_width="match_parent" - android:layout_marginTop="32dp" + android:layout_marginTop="48dp" android:layout_marginHorizontal="16dp" android:layout_marginBottom="24dp" android:textSize="14sp" diff --git a/app/src/main/res/layout/trackers_item_graph.xml b/app/src/main/res/layout/trackers_item_graph.xml index aabc1084..aeef4f62 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/values/colors.xml b/app/src/main/res/values/colors.xml index 54da383b..3c3ca875 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/strings.xml b/app/src/main/res/values/strings.xml index 08ea0b5a..c48d0e2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ ~ along with this program. If not, see . --> - Advanced Privacy + Advanced Privacy_2 Close @@ -93,10 +93,21 @@ Manage apps\' trackers 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 - %d trackers + 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 + + + %d trackers @string/ipscrambling_app_list_infos Apps diff --git a/build.gradle b/build.gradle index 1f0ab169..7b85fe17 100644 --- a/build.gradle +++ b/build.gradle @@ -89,7 +89,7 @@ subprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { // Treat all Kotlin warnings as errors - allWarningsAsErrors = true + //TODO allWarningsAsErrors = true freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" // Set JVM target to 1.8 -- GitLab From 431170f016ee372294e73286fb625a4ca28dde1d Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Mon, 4 Dec 2023 09:11:07 +0100 Subject: [PATCH 3/8] 1465: Request trackers data by period. --- .../usecases/TrackersAndAppsListsUseCase.kt | 17 +++++++++++++ .../features/trackers/TrackersState.kt | 14 +++++++++++ .../trackers/data/StatsDatabase.kt | 25 +++++++++++++++---- 3 files changed, 51 insertions(+), 5 deletions(-) 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 ea07e8fa..27f96c45 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 @@ -31,6 +31,23 @@ class TrackersAndAppsListsUseCase( private val trackersRepository: TrackersRepository, private val appListsRepository: AppListsRepository, ) { +// enum class Period + + + + suspend fun getAppsAndTrackersCounts(period: TODO): TrackersAndAppsLists { + val periodStart: Instant + val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart) + val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) + val (countByApp, countByTracker) = foldToCountByEntityMaps(trackersAndApps) + + return TrackersAndAppsLists( + trackers = buildTrackerList(countByTracker), + allApps = buildAllAppList(countByApp), + appsWithTrackers = buildAppList(countByApp) + ) + } + suspend fun getTrackersAndAppsLists(): TrackersAndAppsLists { val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp() 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 1685c081..bcf05970 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,6 +18,7 @@ package foundation.e.advancedprivacy.features.trackers +import androidx.annotation.StringRes import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics import foundation.e.advancedprivacy.trackers.domain.entities.Tracker @@ -32,6 +33,19 @@ data class TrackersState( val hideNoTrackersApps: Boolean = false ) +data class TrackersPeriodState( + @StringRes val tabLabel: Int = 0, + @StringRes val title: Int = 0, + val callsBlockedNLeaked: List>, + val periods: List, + val trackersCount: Int, + val graduations: List? = null + val allApps: List? = null, + val trackers: List? = null, + val appsWithTrackers: List? = null, + val hideNoTrackersApps: Boolean = false +) + data class AppWithTrackersCount( val app: ApplicationDescription, val trackersCount: Int = 0 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 a80d4dc0..abd2795d 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 @@ -275,14 +276,28 @@ 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 cursor = db.rawQuery( +// "SELECT DISTINCT $projection FROM $TABLE_NAME", // + +// arrayOf() +// ) val res = mutableListOf>() while (cursor.moveToNext()) { -- GitLab From ee5825055d359bdb737359d7b02de79342fdf9f7 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Mon, 18 Dec 2023 09:19:35 +0100 Subject: [PATCH 4/8] 1465: move tracker page on whole tracker page. --- .../e/advancedprivacy/KoinModule.kt | 13 ++ .../entities/TrackersPeriodicStatistics.kt | 5 +- .../usecases/TrackersAndAppsListsUseCase.kt | 31 ++- .../usecases/TrackersStatisticsUseCase.kt | 46 ++-- .../features/trackers/AppsAdapter.kt | 4 +- .../features/trackers/GraphHolder.kt | 17 +- .../features/trackers/ListsTabPagerAdapter.kt | 12 +- .../features/trackers/TrackersAdapter.kt | 4 +- .../features/trackers/TrackersFragment.kt | 83 ++------ .../features/trackers/TrackersGraphAdapter.kt | 43 ---- .../trackers/TrackersPeriodAdapter.kt | 59 ++++++ .../trackers/TrackersPeriodFragment.kt | 196 ++++++++++++++++++ .../trackers/TrackersPeriodViewModel.kt | 108 ++++++++++ .../features/trackers/TrackersState.kt | 40 ++-- .../features/trackers/TrackersViewModel.kt | 58 +----- app/src/main/res/layout/fragment_trackers.xml | 36 +--- .../main/res/layout/trackers_item_graph.xml | 2 +- .../res/layout/trackers_period_fragment.xml | 64 ++++++ app/src/main/res/navigation/nav_graph.xml | 9 + app/src/main/res/values/strings.xml | 2 +- .../trackers/data/StatsDatabase.kt | 49 +++++ 21 files changed, 626 insertions(+), 255 deletions(-) delete mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt create mode 100644 app/src/main/res/layout/trackers_period_fragment.xml diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 55183e9a..03cab554 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -42,6 +42,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 @@ -172,6 +174,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/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/entities/TrackersPeriodicStatistics.kt index 001527ba..b0726451 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 @@ -17,14 +17,11 @@ package foundation.e.advancedprivacy.domain.entities -import androidx.annotation.StringRes - data class TrackersPeriodicStatistics( // TODO: dont let O for stringres !! - @StringRes val tabLabel: Int = 0, - @StringRes val title: Int = 0, 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 27f96c45..6df9dfdb 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,11 +20,13 @@ 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, @@ -33,10 +35,8 @@ class TrackersAndAppsListsUseCase( ) { // enum class Period - - - suspend fun getAppsAndTrackersCounts(period: TODO): TrackersAndAppsLists { - val periodStart: Instant + 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) @@ -48,18 +48,17 @@ class TrackersAndAppsListsUseCase( ) } - - suspend fun getTrackersAndAppsLists(): TrackersAndAppsLists { - val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp() - val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) - val (countByApp, countByTracker) = foldToCountByEntityMaps(trackersAndApps) - - return TrackersAndAppsLists( - trackers = buildTrackerList(countByTracker), - allApps = buildAllAppList(countByApp), - appsWithTrackers = buildAppList(countByApp) - ) - } +// suspend fun getTrackersAndAppsLists(): TrackersAndAppsLists { +// val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp() +// val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) +// val (countByApp, countByTracker) = foldToCountByEntityMaps(trackersAndApps) +// +// return TrackersAndAppsLists( +// trackers = buildTrackerList(countByTracker), +// allApps = buildAllAppList(countByApp), +// appsWithTrackers = buildAppList(countByApp) +// ) +// } private fun buildTrackerList(countByTracker: Map): List { return countByTracker.map { (tracker, count) -> 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 ebaa460f..d55c7bef 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 @@ -19,12 +19,12 @@ package foundation.e.advancedprivacy.domain.usecases import android.content.res.Resources -import android.util.Log import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.throttleFirst import foundation.e.advancedprivacy.data.repositories.AppListsRepository 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 @@ -93,6 +93,14 @@ 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) @@ -105,10 +113,7 @@ class TrackersStatisticsUseCase( periods.add(if (start.hour % 6 == 0) formatter.format(start) else null) end = start.minus(1, ChronoUnit.MINUTES) } - return periods.reversed().let { - Log.d("DebugPeriod", "day periods: $it") - it - } + return periods.reversed() } private fun buildMonthGraduations(): List { @@ -123,10 +128,7 @@ class TrackersStatisticsUseCase( periods.add(if ((start.dayOfMonth) % 6 == 0) formatter.format(start) else null) end = start.minus(1, ChronoUnit.HOURS) } - return periods.reversed().let { - Log.d("DebugPeriod", "month periods: $it") - it - } + return periods.reversed() } private fun buildYearGraduations(): List { @@ -139,17 +141,19 @@ class TrackersStatisticsUseCase( var end = ZonedDateTime.now() for (i in 1..12) { val start = end.truncatedTo(ChronoUnit.DAYS).let { - it.minusDays(it.dayOfMonth.toLong()).let { - Log.d("DebugPeriod", "start: $it") - it - } + 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().let { - Log.d("DebugPeriod", "years periods: $it") - it + return periods.reversed() + } + + private fun buildLabels(period: Period): List { + return when (period) { + Period.DAY -> buildDayLabels() + Period.MONTH -> buildMonthLabels() + Period.YEAR -> buildYearLabels() } } @@ -193,6 +197,16 @@ class TrackersStatisticsUseCase( return periods.reversed() } + 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), + ) + } + fun getDayMonthYearStatistics(): Triple { return Triple( TrackersPeriodicStatistics( 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 f00dff8f..e27bf628 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 @@ -25,11 +25,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/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt index ea244cbd..ae7cee70 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt @@ -28,7 +28,6 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.text.toSpannable import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import com.github.mikephil.charting.components.AxisBase import com.github.mikephil.charting.components.MarkerView import com.github.mikephil.charting.components.XAxis @@ -45,12 +44,11 @@ import com.github.mikephil.charting.utils.MPPointF import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.extensions.dpToPxF import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding -import foundation.e.advancedprivacy.domain.entities.TrackersPeriodicStatistics import kotlin.math.floor -class GraphHolder(private val binding: TrackersItemGraphBinding) : RecyclerView.ViewHolder(binding.root) { +class GraphHolder(private val binding: TrackersItemGraphBinding) { - private val context = itemView.context + private val context = binding.root.context private val barChart = binding.graph private var data = emptyList>() @@ -245,15 +243,16 @@ class GraphHolder(private val binding: TrackersItemGraphBinding) : RecyclerView. barChart.invalidate() } - fun onBind(state: TrackersPeriodicStatistics) { + fun onBind(state: TrackersPeriodState) { with(binding) { - title.text = itemView.context.getString(state.title) + title.text = context.getString(state.title) val views = listOf( helperText, legendBlockedIcon, legendBlocked, legendAllowedIcon, legendAllowed, trackersDetected, trackersAllowed ) + if (state.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) { graph.visibility = View.INVISIBLE graphEmpty.isVisible = true @@ -271,8 +270,10 @@ class GraphHolder(private val binding: TrackersItemGraphBinding) : RecyclerView. R.string.trackers_graph_detected_trackers, state.trackersCount ) - trackersAllowed.text = - context.getString(R.string.trackers_graph_allowed_trackers, -12) + trackersAllowed.text = context.getString( + R.string.trackers_graph_allowed_trackers, + state.trackersAllowedCount + ) } } } 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 e9a046f7..dd665fc9 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 3270bf32..135d43e0 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 74f0557f..fc243a70 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 @@ -52,41 +52,18 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private lateinit var binding: FragmentTrackersBinding - private lateinit var graphsAdapter: TrackersGraphAdapter - - 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) - 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() + val trackersTabs = binding.trackersPeriodsTabs + val trackersPager = binding.trackersPeriodsPager - binding.listsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageScrollStateChanged(state: Int) { - super.onPageScrollStateChanged(state) - if (state == ViewPager2.SCROLL_STATE_IDLE) { - updatePagerHeight() - } - } - }) - - val trackersTabs = binding.trackersDurationTabs - val trackersPager = binding.trackersDurationPager - - graphsAdapter = TrackersGraphAdapter(requireContext()) - trackersPager.adapter = graphsAdapter + pagerAdapter = TrackersPeriodAdapter(this) + trackersPager.adapter = pagerAdapter TabLayoutMediator(trackersTabs, trackersPager) { tab, position -> // TODO tab.text = getDisplayDuration(position) @@ -104,6 +81,15 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { setupTrackersInfos() + binding.trackersPeriodsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageScrollStateChanged(state: Int) { + super.onPageScrollStateChanged(state) + if (state == ViewPager2.SCROLL_STATE_IDLE) { + updatePagerHeight() + } + } + }) + listenViewModel() } @@ -188,17 +174,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) } @@ -207,6 +193,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener) oldPosition = position + adapter?.notifyItemChanged(position) } } @@ -217,9 +204,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { @SuppressLint("NotifyDataSetChanged") private fun render(state: TrackersState) { - graphsAdapter.setState(state) - - tabAdapter.updateDataSet(state) + // pagerAdapter.setState(state) updatePagerHeight() } @@ -231,32 +216,4 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { else -> "" } } - -// 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) - } - } - } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt deleted file mode 100644 index 6f044dcf..00000000 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersGraphAdapter.kt +++ /dev/null @@ -1,43 +0,0 @@ -package foundation.e.advancedprivacy.features.trackers - -import android.content.Context -import android.util.Log -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding - -class TrackersGraphAdapter( - private val context: Context -) : RecyclerView.Adapter() { - - private var state: TrackersState? = null - - fun setState(state: TrackersState) { - this.state = state - notifyDataSetChanged() - } - -// override fun getItemViewType(position: Int): Int { -// return position -// } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphHolder { - Log.d("DebugPeriod", "onCreateViewHolder viewType: $viewType") - val binding = TrackersItemGraphBinding.inflate(LayoutInflater.from(context), parent, false) - return GraphHolder(binding) - } - - override fun getItemCount(): Int { - return 3 - } - - override fun onBindViewHolder(holder: GraphHolder, position: Int) { - when (position) { - 0 -> state?.dayStatistics - 1 -> state?.monthStatistics - 2 -> state?.yearStatistics - else -> null - }?.let { holder.onBind(it) } - } -} 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 00000000..d9cee649 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt @@ -0,0 +1,59 @@ +package foundation.e.advancedprivacy.features.trackers + +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter + +class TrackersPeriodAdapter( + context: Fragment +) : FragmentStateAdapter(context) { + + override fun getItemCount(): Int { + return 3 + } + override fun createFragment(position: Int): Fragment { + // TODO: position controled by viewModel ? + + return TrackersPeriodFragment().apply { + arguments = TrackersPeriodFragment.buildArguments(period = getPeriod(position)) + } + } + + private fun getPeriod(position: Int): Period { + return when (position) { + 0 -> Period.DAY + 1 -> Period.MONTH + 2 -> Period.YEAR + else -> Period.DAY + } + } + +// private var state: TrackersState? = null +// +// fun setState(state: TrackersState) { +// this.state = state +// notifyDataSetChanged() +// } + +// override fun getItemViewType(position: Int): Int { +// return position +// } +// +// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphHolder { +// Log.d("DebugPeriod", "onCreateViewHolder viewType: $viewType") +// val binding = TrackersItemGraphBinding.inflate(LayoutInflater.from(context), parent, false) +// return GraphHolder(binding) +// } +// +// override fun getItemCount(): Int { +// return 3 +// } +// +// override fun onBindViewHolder(holder: GraphHolder, position: Int) { +// when (position) { +// 0 -> state?.dayStatistics +// 1 -> state?.monthStatistics +// 2 -> state?.yearStatistics +// else -> null +// }?.let { holder.onBind(it) } +// } +} 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 00000000..3dbafc03 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodFragment.kt @@ -0,0 +1,196 @@ +/* + * 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.util.Log +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 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) + Log.d("DebugPeriod", "TrackersPeriodFragment::onViewCreated - ${requireArguments().getString(ARG_PERIOD)}") + + 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 + adapter?.notifyItemChanged(position) + } + } + + private fun displayToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + + @SuppressLint("NotifyDataSetChanged") + private fun render(state: TrackersPeriodState) { + Log.d("DebugPeriod", "TrackersPeriodFragment::render - ${getString(state.title)}") + 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 00000000..cc64e17d --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodViewModel.kt @@ -0,0 +1,108 @@ +/* + * 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 android.util.Log +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) { + Log.d("DebugPeriod", "TrackersPeriodViewModel::doOnStartedState") + trackersStatisticsUseCase.listenUpdates().collect { + Log.d("DebugPeriod", "TrackersPeriodViewModel::doOnStartedState listenUpdates") + 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 -> + Log.d("DebugPeriod", "TrackersPeriodViewModel::doOnStartedState getAppsAndTrackersCounts") + _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 bcf05970..5bde8786 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersState.kt @@ -20,26 +20,24 @@ package foundation.e.advancedprivacy.features.trackers import androidx.annotation.StringRes 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, - val allApps: List? = null, - val trackers: List? = null, - val appsWithTrackers: List? = null, - val hideNoTrackersApps: Boolean = false + val periods: List = emptyList(), ) data class TrackersPeriodState( @StringRes val tabLabel: Int = 0, @StringRes val title: Int = 0, - val callsBlockedNLeaked: List>, - val periods: List, - val trackersCount: Int, - val graduations: List? = null + 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, @@ -55,3 +53,21 @@ 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 b159d538..dadcd06d 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,17 +22,13 @@ 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 @@ -41,7 +37,16 @@ class TrackersViewModel( private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase ) : ViewModel() { - private val _state = MutableStateFlow(TrackersState()) + private val _state = MutableStateFlow( + TrackersState( + periods = listOf( + Period.DAY, + Period.MONTH, + Period.YEAR + ) + ) + ) + val state = _state.asStateFlow() private val _singleEvents = MutableSharedFlow() @@ -51,55 +56,12 @@ class TrackersViewModel( 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.copy( - tabLabel = R.string.trackers_period_day, - title = R.string.trackers_graph_title_day, - ), - monthStatistics = month.copy( - tabLabel = R.string.trackers_period_month, - title = R.string.trackers_graph_title_month, - ), - yearStatistics = year.copy( - tabLabel = R.string.trackers_period_year, - title = R.string.trackers_graph_title_year, - ), - ) - } - } - - trackersAndAppsListsUseCase.getTrackersAndAppsLists().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 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/res/layout/fragment_trackers.xml b/app/src/main/res/layout/fragment_trackers.xml index eeefa891..de169733 100644 --- a/app/src/main/res/layout/fragment_trackers.xml +++ b/app/src/main/res/layout/fragment_trackers.xml @@ -37,6 +37,7 @@ android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical" + android:background="@android:color/holo_green_dark" > - - - - diff --git a/app/src/main/res/layout/trackers_item_graph.xml b/app/src/main/res/layout/trackers_item_graph.xml index aeef4f62..b21371d2 100644 --- a/app/src/main/res/layout/trackers_item_graph.xml +++ b/app/src/main/res/layout/trackers_item_graph.xml @@ -20,7 +20,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" > + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 1047da66..5630d0e6 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -99,4 +99,13 @@ android:label="@string/dashboard_apps_permissions_title" app:action="android.intent.action.MANAGE_PERMISSIONS" /> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c48d0e2b..1452a407 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ ~ along with this program. If not, see . --> - Advanced Privacy_2 + Advanced Privacy_3 Close 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 abd2795d..d56da9a9 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 @@ -24,6 +24,7 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.provider.BaseColumns +import android.util.Log import androidx.core.database.getStringOrNull import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APPID import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_BLOCKED @@ -167,6 +168,7 @@ class StatsDatabase( periodsCount: Int, periodUnit: TemporalUnit ): List> { + Log.d("DebugPeriod", "getTrackersCallsOnPeriod: periodsCount: $periodsCount") var sqlitePeriodFormat = "%Y%m" var javaPeriodFormat = "yyyyMM" if (periodUnit === ChronoUnit.MONTHS) { @@ -207,6 +209,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 -- GitLab From 694e6b832cbf7a58c056b03e99076b393f15e9b0 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Wed, 20 Dec 2023 22:27:22 +0100 Subject: [PATCH 5/8] 1465: Cleanup code. --- .../entities/TrackersPeriodicStatistics.kt | 2 +- .../usecases/TrackersAndAppsListsUseCase.kt | 14 ------ .../usecases/TrackersStatisticsUseCase.kt | 28 +----------- .../features/trackers/AppsAdapter.kt | 3 +- .../features/trackers/TrackersFragment.kt | 43 +------------------ .../trackers/TrackersPeriodAdapter.kt | 32 -------------- .../trackers/TrackersPeriodFragment.kt | 3 -- .../trackers/TrackersPeriodViewModel.kt | 4 -- .../features/trackers/TrackersState.kt | 9 ++-- .../features/trackers/TrackersViewModel.kt | 33 ++++---------- .../main/res/drawable/ic_legend_blocked_2.xml | 1 + app/src/main/res/layout/chart_tooltip_2.xml | 1 + app/src/main/res/layout/fragment_trackers.xml | 1 - .../main/res/layout/trackers_item_graph.xml | 1 + .../res/layout/trackers_period_fragment.xml | 5 +-- app/src/main/res/navigation/nav_graph.xml | 9 ---- app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-is/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-night/colors.xml | 2 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values/colors.xml | 2 - app/src/main/res/values/strings.xml | 10 ++--- build.gradle | 2 +- .../trackers/data/StatsDatabase.kt | 6 --- 29 files changed, 27 insertions(+), 192 deletions(-) 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 b0726451..95c5c3d8 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 @@ -18,7 +19,6 @@ package foundation.e.advancedprivacy.domain.entities data class TrackersPeriodicStatistics( - // TODO: dont let O for stringres !! val callsBlockedNLeaked: List>, val periods: List, val trackersCount: Int, 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 6df9dfdb..9bff2f7e 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 @@ -33,8 +33,6 @@ class TrackersAndAppsListsUseCase( private val trackersRepository: TrackersRepository, private val appListsRepository: AppListsRepository, ) { -// enum class Period - suspend fun getAppsAndTrackersCounts(period: Period): TrackersAndAppsLists { val periodStart: Instant = period.getPeriodStart() val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp(periodStart) @@ -48,18 +46,6 @@ class TrackersAndAppsListsUseCase( ) } -// suspend fun getTrackersAndAppsLists(): TrackersAndAppsLists { -// val trackersAndAppsIds = statsDatabase.getDistinctTrackerAndApp() -// val trackersAndApps = mapIdsToEntities(trackersAndAppsIds) -// val (countByApp, countByTracker) = foldToCountByEntityMaps(trackersAndApps) -// -// return TrackersAndAppsLists( -// trackers = buildTrackerList(countByTracker), -// allApps = buildAllAppList(countByApp), -// appsWithTrackers = buildAppList(countByApp) -// ) -// } - private fun buildTrackerList(countByTracker: Map): List { return countByTracker.map { (tracker, count) -> TrackerWithAppsCount(tracker = tracker, appsCount = count) diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt index d55c7bef..60063403 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 @@ -118,7 +118,7 @@ class TrackersStatisticsUseCase( private fun buildMonthGraduations(): List { val formatter = DateTimeFormatter.ofPattern( - "MM/dd" + resources.getString(R.string.trackers_graph_month_graduations_format) ) val periods = mutableListOf() @@ -133,8 +133,7 @@ class TrackersStatisticsUseCase( private fun buildYearGraduations(): List { val formatter = DateTimeFormatter.ofPattern( - "MMM" - // resources.getString(R.string.trackers_graph_hours_period_format) // TODO. + resources.getString(R.string.trackers_graph_year_graduations_format) ) val periods = mutableListOf() @@ -207,29 +206,6 @@ class TrackersStatisticsUseCase( ) } - fun getDayMonthYearStatistics(): Triple { - return Triple( - TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS), - periods = buildDayLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS), - graduations = buildDayGraduations(), - ), - TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(30, ChronoUnit.DAYS), - periods = buildMonthLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(30, ChronoUnit.DAYS), - graduations = buildMonthGraduations(), - ), - TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(12, ChronoUnit.MONTHS), - periods = buildYearLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(12, ChronoUnit.MONTHS), - graduations = buildYearGraduations(), - ) - ) - } - suspend fun isWhiteListEmpty(app: ApplicationDescription): Boolean { return appListsRepository.mapReduceForHiddenApps( app = app, 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 e27bf628..dcfd8176 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 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 fc243a70..c1c05b15 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 @@ -18,7 +18,6 @@ package foundation.e.advancedprivacy.features.trackers -import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle @@ -37,7 +36,6 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.NavToolbarFragment @@ -65,20 +63,9 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { pagerAdapter = TrackersPeriodAdapter(this) trackersPager.adapter = pagerAdapter TabLayoutMediator(trackersTabs, trackersPager) { tab, position -> - // TODO - tab.text = getDisplayDuration(position) + tab.text = getString(viewModel.getDisplayDuration(position)) }.attach() - trackersTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab?) { - // tabSelection.postValue(tab?.position ?: -1) - } - - override fun onTabUnselected(tab: TabLayout.Tab?) {} - - override fun onTabReselected(tab: TabLayout.Tab?) {} - }) - setupTrackersInfos() binding.trackersPeriodsPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { @@ -95,13 +82,6 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { 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) @@ -113,12 +93,6 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { viewModel.navigate.collect(findNavController()::navigate) } } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } } } @@ -201,19 +175,4 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { private fun displayToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } - - @SuppressLint("NotifyDataSetChanged") - private fun render(state: TrackersState) { - // pagerAdapter.setState(state) - updatePagerHeight() - } - - private fun getDisplayDuration(position: Int): String { - return when (position) { - 0 -> getString(R.string.trackers_period_day) - 1 -> getString(R.string.trackers_period_month) - 2 -> getString(R.string.trackers_period_year) - else -> "" - } - } } 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 index d9cee649..322dcf43 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt @@ -11,8 +11,6 @@ class TrackersPeriodAdapter( return 3 } override fun createFragment(position: Int): Fragment { - // TODO: position controled by viewModel ? - return TrackersPeriodFragment().apply { arguments = TrackersPeriodFragment.buildArguments(period = getPeriod(position)) } @@ -26,34 +24,4 @@ class TrackersPeriodAdapter( else -> Period.DAY } } - -// private var state: TrackersState? = null -// -// fun setState(state: TrackersState) { -// this.state = state -// notifyDataSetChanged() -// } - -// override fun getItemViewType(position: Int): Int { -// return position -// } -// -// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GraphHolder { -// Log.d("DebugPeriod", "onCreateViewHolder viewType: $viewType") -// val binding = TrackersItemGraphBinding.inflate(LayoutInflater.from(context), parent, false) -// return GraphHolder(binding) -// } -// -// override fun getItemCount(): Int { -// return 3 -// } -// -// override fun onBindViewHolder(holder: GraphHolder, position: Int) { -// when (position) { -// 0 -> state?.dayStatistics -// 1 -> state?.monthStatistics -// 2 -> state?.yearStatistics -// else -> null -// }?.let { holder.onBind(it) } -// } } 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 3dbafc03..e7290015 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 @@ -22,7 +22,6 @@ import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.View import android.view.ViewTreeObserver import android.widget.Toast @@ -69,7 +68,6 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Log.d("DebugPeriod", "TrackersPeriodFragment::onViewCreated - ${requireArguments().getString(ARG_PERIOD)}") binding = TrackersPeriodFragmentBinding.bind(view) @@ -178,7 +176,6 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { @SuppressLint("NotifyDataSetChanged") private fun render(state: TrackersPeriodState) { - Log.d("DebugPeriod", "TrackersPeriodFragment::render - ${getString(state.title)}") graphHolder.onBind(state) tabAdapter.updateDataSet(state) updatePagerHeight() 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 cc64e17d..b96433de 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 @@ -19,7 +19,6 @@ package foundation.e.advancedprivacy.features.trackers import android.net.Uri -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections @@ -61,9 +60,7 @@ class TrackersPeriodViewModel( val navigate = _navigate.asSharedFlow() suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - Log.d("DebugPeriod", "TrackersPeriodViewModel::doOnStartedState") trackersStatisticsUseCase.listenUpdates().collect { - Log.d("DebugPeriod", "TrackersPeriodViewModel::doOnStartedState listenUpdates") trackersStatisticsUseCase.getGraphData(period).let { graphData -> _state.update { it.copy( @@ -77,7 +74,6 @@ class TrackersPeriodViewModel( } trackersAndAppsListsUseCase.getAppsAndTrackersCounts(period).let { lists -> - Log.d("DebugPeriod", "TrackersPeriodViewModel::doOnStartedState getAppsAndTrackersCounts") _state.update { it.copy( trackers = lists.trackers, 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 5bde8786..f9e6407c 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 @@ -19,6 +19,7 @@ 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 java.time.Instant @@ -26,13 +27,9 @@ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit import java.time.temporal.TemporalUnit -data class TrackersState( - val periods: List = emptyList(), -) - data class TrackersPeriodState( - @StringRes val tabLabel: Int = 0, - @StringRes val title: Int = 0, + @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, 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 dadcd06d..a49da849 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,42 +22,25 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections -import foundation.e.advancedprivacy.domain.usecases.TrackersAndAppsListsUseCase -import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase -import kotlinx.coroutines.Dispatchers +import foundation.e.advancedprivacy.R import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class TrackersViewModel( - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val trackersAndAppsListsUseCase: TrackersAndAppsListsUseCase -) : ViewModel() { - - private val _state = MutableStateFlow( - TrackersState( - periods = listOf( - Period.DAY, - Period.MONTH, - Period.YEAR - ) - ) - ) - - val state = _state.asStateFlow() +class TrackersViewModel() : ViewModel() { private val _singleEvents = MutableSharedFlow() val singleEvents = _singleEvents.asSharedFlow() private val _navigate = MutableSharedFlow() val navigate = _navigate.asSharedFlow() - suspend fun doOnStartedState() = withContext(Dispatchers.IO) { + 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 onClickLearnMore() = viewModelScope.launch { _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS))) } diff --git a/app/src/main/res/drawable/ic_legend_blocked_2.xml b/app/src/main/res/drawable/ic_legend_blocked_2.xml index d1ae0152..9a146a8b 100644 --- a/app/src/main/res/drawable/ic_legend_blocked_2.xml +++ b/app/src/main/res/drawable/ic_legend_blocked_2.xml @@ -1,4 +1,5 @@ - Advanced Privacy_3 + Advanced Privacy + Close OK @@ -106,10 +107,6 @@ %d detected trackers %d allowed - - %d trackers - @string/ipscrambling_app_list_infos - Apps Trackers Trackers Activity Summary @@ -122,6 +119,9 @@ MMMM d - EEE MMMM yyyy + MM/dd + MMM + %s tracking summary Total diff --git a/build.gradle b/build.gradle index 7b85fe17..1f0ab169 100644 --- a/build.gradle +++ b/build.gradle @@ -89,7 +89,7 @@ subprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { // Treat all Kotlin warnings as errors - //TODO allWarningsAsErrors = true + allWarningsAsErrors = true freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" // Set JVM target to 1.8 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 d56da9a9..dc726db3 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 @@ -24,7 +24,6 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.provider.BaseColumns -import android.util.Log import androidx.core.database.getStringOrNull import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_APPID import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_NUMBER_BLOCKED @@ -168,7 +167,6 @@ class StatsDatabase( periodsCount: Int, periodUnit: TemporalUnit ): List> { - Log.d("DebugPeriod", "getTrackersCallsOnPeriod: periodsCount: $periodsCount") var sqlitePeriodFormat = "%Y%m" var javaPeriodFormat = "yyyyMM" if (periodUnit === ChronoUnit.MONTHS) { @@ -343,10 +341,6 @@ class StatsDatabase( null ) -// val cursor = db.rawQuery( -// "SELECT DISTINCT $projection FROM $TABLE_NAME", // + -// arrayOf() -// ) val res = mutableListOf>() while (cursor.moveToNext()) { -- GitLab From 9fc23d0473cb8ffed464629e9ef7039590a55202 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Tue, 26 Dec 2023 17:03:46 +0100 Subject: [PATCH 6/8] 1465: save last seen period, fix refresh view height --- .../e/advancedprivacy/KoinModule.kt | 2 ++ .../data/repositories/LocalStateRepository.kt | 5 +++ .../domain/usecases/TrackersScreenUseCase.kt | 32 +++++++++++++++++++ .../features/trackers/TrackersFragment.kt | 12 +++++-- .../trackers/TrackersPeriodAdapter.kt | 16 +++------- .../trackers/TrackersPeriodFragment.kt | 2 +- .../features/trackers/TrackersViewModel.kt | 28 +++++++++++++++- .../repositories/LocalStateRepository.kt | 2 ++ 8 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index 03cab554..eb225b15 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -34,6 +34,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 @@ -143,6 +144,7 @@ val appModule = module { singleOf(::AppTrackersUseCase) singleOf(::TrackerDetailsUseCase) + singleOf(::TrackersScreenUseCase) single { PermissionsPrivacyModuleImpl(context = androidContext()) 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 2afd6eea..c131eef5 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/domain/usecases/TrackersScreenUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt new file mode 100644 index 00000000..d6290950 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersScreenUseCase.kt @@ -0,0 +1,32 @@ +/* + * 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/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt index c1c05b15..063458da 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 @@ -60,7 +60,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { val trackersTabs = binding.trackersPeriodsTabs val trackersPager = binding.trackersPeriodsPager - pagerAdapter = TrackersPeriodAdapter(this) + pagerAdapter = TrackersPeriodAdapter(this, viewModel) trackersPager.adapter = pagerAdapter TabLayoutMediator(trackersTabs, trackersPager) { tab, position -> tab.text = getString(viewModel.getDisplayDuration(position)) @@ -73,6 +73,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { super.onPageScrollStateChanged(state) if (state == ViewPager2.SCROLL_STATE_IDLE) { updatePagerHeight() + viewModel.onDisplayedItemChanged(binding.trackersPeriodsPager.currentItem) } } }) @@ -80,6 +81,13 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { listenViewModel() } + override fun onResume() { + super.onResume() + lifecycleScope.launch { + binding.trackersPeriodsPager.currentItem = viewModel.getLastPosition() + } + } + private fun listenViewModel() { with(viewLifecycleOwner) { lifecycleScope.launch { @@ -168,7 +176,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { oldPosition = position - adapter?.notifyItemChanged(position) + binding.trackersPeriodsPager.requestLayout() } } 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 index 322dcf43..7d629870 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt @@ -4,24 +4,16 @@ import androidx.fragment.app.Fragment import androidx.viewpager2.adapter.FragmentStateAdapter class TrackersPeriodAdapter( - context: Fragment + context: Fragment, + private val viewModel: TrackersViewModel ) : FragmentStateAdapter(context) { override fun getItemCount(): Int { - return 3 + return viewModel.positionsCount } override fun createFragment(position: Int): Fragment { return TrackersPeriodFragment().apply { - arguments = TrackersPeriodFragment.buildArguments(period = getPeriod(position)) - } - } - - private fun getPeriod(position: Int): Period { - return when (position) { - 0 -> Period.DAY - 1 -> Period.MONTH - 2 -> Period.YEAR - else -> Period.DAY + 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 index e7290015..f67eba3d 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 @@ -166,7 +166,7 @@ class TrackersPeriodFragment : Fragment(R.layout.trackers_period_fragment) { newItem?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener) oldPosition = position - adapter?.notifyItemChanged(position) + binding.listsPager.requestLayout() } } 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 a49da849..d9b7b8b7 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 @@ -23,17 +23,29 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections 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.asSharedFlow import kotlinx.coroutines.launch -class TrackersViewModel() : ViewModel() { +class TrackersViewModel(private val trackersScreenUseCase: TrackersScreenUseCase) : ViewModel() { private val _singleEvents = MutableSharedFlow() val singleEvents = _singleEvents.asSharedFlow() private val _navigate = MutableSharedFlow() val navigate = _navigate.asSharedFlow() + val positionsCount = 3 + fun getPeriod(position: Int): Period { + return when (position) { + 0 -> Period.DAY + 1 -> Period.MONTH + 2 -> Period.YEAR + else -> Period.DAY + } + } + fun getDisplayDuration(position: Int): Int { return when (position) { 0 -> R.string.trackers_period_day @@ -41,6 +53,20 @@ class TrackersViewModel() : ViewModel() { else -> R.string.trackers_period_year } } + + suspend fun getLastPosition(): Int { + val lastPosition = trackersScreenUseCase.getLastPosition() + return if (lastPosition in 0 until positionsCount) { + lastPosition + } else { + 0 + } + } + + 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))) } 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 0266f850..86e1acf9 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 } -- GitLab From f0311cd3e88f44c244b9988841f6e05c015439e8 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Wed, 27 Dec 2023 17:23:21 +0100 Subject: [PATCH 7/8] 1465: Protect bad date formating from translation, add translations. --- .../e/advancedprivacy/KoinModule.kt | 2 ++ .../data/repositories/ResourcesGetter.kt | 30 +++++++++++++++++ .../usecases/TrackersStatisticsUseCase.kt | 32 +++++++------------ app/src/main/res/values-de/strings.xml | 3 ++ app/src/main/res/values-en-rUS/strings.xml | 9 ++++++ app/src/main/res/values-es/strings.xml | 3 ++ app/src/main/res/values-fi/strings.xml | 2 ++ app/src/main/res/values-fr/strings.xml | 9 ++++-- app/src/main/res/values-is/strings.xml | 3 ++ app/src/main/res/values-it/strings.xml | 5 ++- app/src/main/res/values-nl/strings.xml | 7 ++++ app/src/main/res/values-ru/strings.xml | 3 ++ app/src/main/res/values-sv/strings.xml | 5 ++- app/src/main/res/values/strings.xml | 10 +++--- 14 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt create mode 100644 app/src/main/res/values-en-rUS/strings.xml diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index eb225b15..4c7f18e3 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 @@ -113,6 +114,7 @@ val appModule = module { } single { CityDataSource } + single { ResourcesGetter(androidContext()) } singleOf(::AppListUseCase) single { 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 00000000..20923ccb --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt @@ -0,0 +1,30 @@ +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/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt index 60063403..43337f6e 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,10 +18,10 @@ 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 @@ -36,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 @@ -47,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() @@ -102,9 +101,7 @@ class TrackersStatisticsUseCase( } 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() @@ -117,8 +114,8 @@ class TrackersStatisticsUseCase( } private fun buildMonthGraduations(): List { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_month_graduations_format) + val formatter = resourcesGetter.getFormatter( + R.string.trackers_graph_month_graduations_format ) val periods = mutableListOf() @@ -132,9 +129,7 @@ class TrackersStatisticsUseCase( } private fun buildYearGraduations(): List { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_year_graduations_format) - ) + val formatter = resourcesGetter.getFormatter(R.string.trackers_graph_year_graduations_format) val periods = mutableListOf() var end = ZonedDateTime.now() @@ -157,9 +152,8 @@ class TrackersStatisticsUseCase( } private fun buildDayLabels(): 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() for (i in 1..24) { @@ -171,9 +165,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) { @@ -184,9 +177,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) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d91e72d2..60e312ae 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -54,6 +54,9 @@ 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 00000000..426be81b --- /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 e86ee424..787e49d6 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -44,6 +44,9 @@ 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 diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 730ea6e9..f9a16c06 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -15,6 +15,8 @@ MMMM yyyy d MMMM EEE HH:mm + dd/MM + MMM kulunut vuosi kulunut kuukausi 24 tuntia diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0ded4846..7f34c62c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -40,9 +40,6 @@ 24 heures mois précédent année précédente - HH:mm - EEE d MMMM - MMMM yyyy Gérer les 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. @@ -135,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 31e3d834..14893dae 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -30,6 +30,9 @@ 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 be29982a..1842dd53 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -43,8 +43,11 @@ 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 11436041..d96d54c0 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 39be8509..2793ec62 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -55,6 +55,9 @@ MMMM yyyy d MMMM EEE HH:mm + dd/MM + MMM + прошлый год прошедший месяц 24 часа diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 77944a75..b5b70984 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -89,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/strings.xml b/app/src/main/res/values/strings.xml index 32f6a074..3df052cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,12 +115,12 @@ %s trackers detected detected in %s apps - HH:mm - MMMM d - EEE - MMMM yyyy + HH:mm + EEE d MMMM + MMMM yyyy - MM/dd - MMM + dd/MM + MMM %s tracking summary -- GitLab From 119ffd6698a6a3483702982db2cc541821269020 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Thu, 28 Dec 2023 16:38:37 +0100 Subject: [PATCH 8/8] 1465: Improve graphholder code readability. --- .../data/repositories/ResourcesGetter.kt | 17 + .../domain/usecases/TrackersScreenUseCase.kt | 1 + .../usecases/TrackersStatisticsUseCase.kt | 2 + .../features/trackers/GraphHolder.kt | 368 ------------------ .../trackers/TrackersPeriodAdapter.kt | 1 + .../trackers/TrackersPeriodFragment.kt | 1 + .../trackers/TrackersPeriodViewModel.kt | 1 + .../features/trackers/TrackersState.kt | 9 +- .../features/trackers/graph/GraphHolder.kt | 294 ++++++++++++++ .../trackers/graph/PeriodMarkerView.kt | 123 ++++++ .../main/res/layout/trackers_item_graph.xml | 1 - 11 files changed, 448 insertions(+), 370 deletions(-) delete mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/GraphHolder.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/trackers/graph/PeriodMarkerView.kt 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 index 20923ccb..6902a993 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/ResourcesGetter.kt @@ -1,3 +1,20 @@ +/* + * 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 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 d6290950..16941443 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 @@ -14,6 +14,7 @@ * 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 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 43337f6e..804ba381 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 @@ -125,6 +125,7 @@ class TrackersStatisticsUseCase( periods.add(if ((start.dayOfMonth) % 6 == 0) formatter.format(start) else null) end = start.minus(1, ChronoUnit.HOURS) } + return periods.reversed() } @@ -140,6 +141,7 @@ class TrackersStatisticsUseCase( periods.add(if (start.monthValue % 3 == 0) formatter.format(start) else null) end = start.minus(1, ChronoUnit.DAYS) } + return periods.reversed() } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt deleted file mode 100644 index ae7cee70..00000000 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/GraphHolder.kt +++ /dev/null @@ -1,368 +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.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.content.ContextCompat -import androidx.core.text.toSpannable -import androidx.core.view.isVisible -import com.github.mikephil.charting.components.AxisBase -import com.github.mikephil.charting.components.MarkerView -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 com.github.mikephil.charting.utils.MPPointF -import foundation.e.advancedprivacy.R -import foundation.e.advancedprivacy.common.extensions.dpToPxF -import foundation.e.advancedprivacy.databinding.TrackersItemGraphBinding -import kotlin.math.floor - -class GraphHolder(private val binding: TrackersItemGraphBinding) { - - private val context = binding.root.context - private val barChart = binding.graph - - private var data = emptyList>() - private var labels = emptyList() - private var graduations: List? = null - - private var isHighlighted = false - - init { - barChart.description = null - barChart.setTouchEnabled(true) - barChart.setScaleEnabled(false) - - barChart.setDrawGridBackground(false) - barChart.setDrawBorders(false) - barChart.axisLeft.isEnabled = false - barChart.axisRight.isEnabled = false - - barChart.legend.isEnabled = false - - barChart.extraTopOffset = 40f - barChart.extraBottomOffset = 4f - - barChart.minOffset = 0f - - val periodMarker = PeriodMarkerView(context) - periodMarker.chartView = barChart - barChart.marker = periodMarker - - barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { - override fun onValueSelected(e: Entry?, h: Highlight?) { - h?.let { - val index = it.x.toInt() - if (index >= 0 && - index < labels.size && - index < this@GraphHolder.data.size - ) { - val period = labels[index] - val (blocked, leaked) = this@GraphHolder.data[index] - periodMarker.setLabel(period, blocked, leaked) - } - } - isHighlighted = true - } - - override fun onNothingSelected() { - isHighlighted = false - } - }) - } - - 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 fun prepareXAxisDashboardDay() { - barChart.offsetTopAndBottom(0) - - val graduationsCount = graduations?.size ?: 2 - - barChart.setXAxisRenderer(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 - run { - var i = 0 - while (i < positions.size) { - positions[i] = mXAxis.mEntries[i / 2] - positions[i + 1] = mXAxis.mEntries[i / 2] - i += 2 - } - } - - mTrans.pointValuesToPixel(positions) - setupGridPaint() - val gridLinePath = mRenderGridLinesPath - gridLinePath.reset() - var i = 0 - while (i < positions.size) { - - val bottomY = if (graduations?.getOrNull( - if (graduationsCount < 24) i / 2 else i - ) != null - ) 0 else 3 - val x = positions[i] - gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 5.dpToPxF(context)) - gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context)) - - c.drawPath(gridLinePath, mGridPaint) - - gridLinePath.reset() - - i += 2 - } - c.restoreToCount(clipRestoreCount) - } - }) - - barChart.setDrawValueAboveBar(false) - 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 (graduationsCount < 24) { - setLabelCount((graduations?.size ?: 24) + 1, true) - - valueFormatter = object : ValueFormatter() { - override fun getAxisLabel(value: Float, axis: AxisBase?): String { - return (graduations?.getOrNull(floor(value).toInt() + 1) ?: "") - } - } - } else { - setLabelCount((graduationsCount / 2) + 1, true) - valueFormatter = 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 refreshDataSet() { - val trackersDataSet = BarDataSet( - data.mapIndexed { index, value -> - BarEntry( - index.toFloat(), - floatArrayOf(value.first.toFloat(), value.second.toFloat()) - ) - }, - "" - ).apply { - - val blockedColor = ContextCompat.getColor(context, R.color.switch_track_on) - val leakedColor = ContextCompat.getColor(context, R.color.red_off) - - colors = listOf( - blockedColor, - leakedColor - ) - - setDrawValues(false) - } - - barChart.data = BarData(trackersDataSet) - prepareYAxis() - prepareXAxisDashboardDay() - - barChart.invalidate() - } - - fun onBind(state: TrackersPeriodState) { - with(binding) { - title.text = context.getString(state.title) - val views = listOf( - helperText, - legendBlockedIcon, legendBlocked, - legendAllowedIcon, legendAllowed, - trackersDetected, trackersAllowed - ) - - if (state.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) { - graph.visibility = View.INVISIBLE - graphEmpty.isVisible = true - views.forEach { it.isVisible = false } - } else { - graph.isVisible = true - graphEmpty.isVisible = false - views.forEach { it.isVisible = true } - data = state.callsBlockedNLeaked - labels = state.periods - graduations = state.graduations - refreshDataSet() - - trackersDetected.text = context.getString( - R.string.trackers_graph_detected_trackers, - state.trackersCount - ) - trackersAllowed.text = context.getString( - R.string.trackers_graph_allowed_trackers, - state.trackersAllowedCount - ) - } - } - } -} - -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/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt index 7d629870..6afc1ba8 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersPeriodAdapter.kt @@ -11,6 +11,7 @@ class TrackersPeriodAdapter( 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 index f67eba3d..995bda36 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 @@ -37,6 +37,7 @@ import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPo 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 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 b96433de..0593e529 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 @@ -51,6 +51,7 @@ class TrackersPeriodViewModel( }, ) ) + val state = _state.asStateFlow() private val _singleEvents = MutableSharedFlow() 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 f9e6407c..f0787fd8 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 @@ -39,7 +39,13 @@ data class TrackersPeriodState( 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, @@ -65,6 +71,7 @@ enum class Period(val periodsCount: Int, val periodUnit: TemporalUnit) { start = start.withDayOfMonth(1) truncatePeriodUnit = ChronoUnit.DAYS } + return start.truncatedTo(truncatePeriodUnit).toInstant() } } 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 00000000..d4483c02 --- /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 00000000..cd7730cb --- /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/layout/trackers_item_graph.xml b/app/src/main/res/layout/trackers_item_graph.xml index 94f1f88b..b21371d2 100644 --- a/app/src/main/res/layout/trackers_item_graph.xml +++ b/app/src/main/res/layout/trackers_item_graph.xml @@ -55,7 +55,6 @@ android:id="@+id/graph" android:layout_marginTop="8dp" android:layout_height="110dp" - android:layout_marginHorizontal="16dp" android:layout_width="match_parent" app:layout_constraintTop_toBottomOf="@+id/title" /> -- GitLab