diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt deleted file mode 100644 index 44fb73d310162d814c41b04bed10a8016f447642..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt +++ /dev/null @@ -1,352 +0,0 @@ -/* - * 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.common - -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.charts.BarChart -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 -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 kotlin.math.floor - -class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) { - var data = emptyList>() - set(value) { - field = value - refreshDataSet() - } - var labels = emptyList() - - 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 - - if (isMarkerAbove) prepareXAxisDashboardDay() else prepareXAxisMarkersBelow() - - val periodMarker = PeriodMarkerView(context, isMarkerAbove) - 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 prepareXAxisDashboardDay() { - barChart.extraTopOffset = 44f - - barChart.offsetTopAndBottom(0) - - 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 - - // Top line - c.drawLine( - mViewPortHandler.contentLeft(), - mViewPortHandler.contentTop(), - mViewPortHandler.contentRight(), - mViewPortHandler.contentTop(), - mAxisLinePaint - ) - - // Bottom line - c.drawLine( - mViewPortHandler.contentLeft(), - mViewPortHandler.contentBottom() - 7.dpToPxF(context), - mViewPortHandler.contentRight(), - mViewPortHandler.contentBottom() - 7.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(i / 2) != null) 0 else 3 - val x = positions[i] - gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 7.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) - setLabelCount(25, true) - textColor = context.getColor(R.color.primary_text) - valueFormatter = object : ValueFormatter() { - override fun getAxisLabel(value: Float, axis: AxisBase?): String { - return graduations?.getOrNull(floor(value).toInt() + 1) ?: "" - } - } - } - } - - private fun prepareXAxisMarkersBelow() { - barChart.extraBottomOffset = 44f - - barChart.offsetTopAndBottom(0) - barChart.setDrawValueAboveBar(false) - - barChart.xAxis.apply { - isEnabled = true - position = XAxis.XAxisPosition.BOTH_SIDED - setDrawGridLines(false) - setDrawLabels(false) - } - } - - fun highlightIndex(index: Int) { - if (index >= 0 && index < data.size) { - val xPx = barChart.getTransformer(YAxis.AxisDependency.LEFT) - .getPixelForValues(index.toFloat(), 0f) - .x - val highlight = Highlight( - index.toFloat(), - 0f, - xPx.toFloat(), - 0f, - 0, - YAxis.AxisDependency.LEFT - ) - - barChart.highlightValue(highlight, true) - } - } - - 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.accent) - val leakedColor = ContextCompat.getColor(context, R.color.red_off) - - colors = listOf( - blockedColor, - leakedColor - ) - - setDrawValues(false) - } - - barChart.data = BarData(trackersDataSet) - barChart.invalidate() - } -} - -class PeriodMarkerView(context: Context, private val isMarkerAbove: Boolean = true) : MarkerView(context, R.layout.chart_tooltip) { - 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 = if (isMarkerAbove) { - 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 - } - } else { - when (position) { - ArrowPosition.LEFT -> R.id.arrow_top_left - ArrowPosition.CENTER -> R.id.arrow_top_center - ArrowPosition.RIGHT -> R.id.arrow_top_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(": $blocked ") - span.setSpan( - ImageSpan(context, R.drawable.ic_legend_blocked, DynamicDrawableSpan.ALIGN_BASELINE), - span.length - 1, - span.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - span.append(" $leaked ") - span.setSpan( - ImageSpan(context, R.drawable.ic_legend_leaked, DynamicDrawableSpan.ALIGN_BASELINE), - span.length - 1, - span.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - 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 = if (isMarkerAbove) { - -posY - } else { - -posY + (chartView?.height?.toFloat() ?: 0f) - height - } - - 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/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersStatisticsUseCase.kt index 64520a502ab7565d23956d4645a6fa67bc2628da..374afe661f3d750a604b0c029465ec8522c7690c 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 @@ -35,8 +35,6 @@ import java.time.temporal.ChronoUnit import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -57,38 +55,6 @@ class TrackersStatisticsUseCase( .throttleFirst(windowDuration = debounce) .onStart { emit(Unit) } - fun getDayStatistics(): Pair { - return TrackersPeriodicStatistics( - callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS), - periods = buildDayLabels(), - trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS), - graduations = buildDayGraduations() - ) to statisticsUseCase.getContactedTrackersCount() - } - - fun getNonBlockedTrackersCount(): Flow { - return if (whitelistRepository.isBlockingEnabled) { - appListsRepository.allApps().map { apps -> - val whiteListedTrackers = mutableSetOf() - val whiteListedApps = whitelistRepository.getWhiteListedApp() - apps.forEach { app -> - if (app in whiteListedApps) { - whiteListedTrackers.addAll(statisticsUseCase.getTrackers(listOf(app))) - } else { - whiteListedTrackers.addAll(getWhiteList(app)) - } - } - whiteListedTrackers.size - } - } else { - flowOf(statisticsUseCase.getContactedTrackersCount()) - } - } - - fun getMostLeakedApp(): ApplicationDescription? { - return statisticsUseCase.getMostLeakedApp(24, ChronoUnit.HOURS) - } - fun getDayTrackersCalls() = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS) fun getDayTrackersCount() = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS) @@ -211,18 +177,12 @@ class TrackersStatisticsUseCase( ) } - suspend fun getCalls(app: ApplicationDescription): Pair { - return appListsRepository.mapReduceForHiddenApps( - app = app, - map = { - statisticsUseCase.getCalls(it, 24, ChronoUnit.HOURS) - }, - reduce = { zip -> - zip.unzip().let { (blocked, leaked) -> - blocked.sum() to leaked.sum() - } - } - ) + suspend fun getLastMonthBlockedLeaksCount(): Int { + return statsDatabase.getBlockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit) + } + + suspend fun getLastMonthAppsWithBLockedLeaksCount(): Int { + return statsDatabase.getAppsWithBLockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit) } private fun getWhiteList(app: ApplicationDescription): List { diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt index 31c4d07cf8c480ec7389335bca63cd81687e758f..9cdffa45859bf95be574e0ad52ef4b05d61db00f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt @@ -19,26 +19,20 @@ package foundation.e.advancedprivacy.features.dashboard import android.os.Bundle -import android.text.Html -import android.text.Html.FROM_HTML_MODE_LEGACY import android.view.View import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.getColor import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs import foundation.e.advancedprivacy.R -import foundation.e.advancedprivacy.common.GraphHolder import foundation.e.advancedprivacy.common.NavToolbarFragment import foundation.e.advancedprivacy.databinding.FragmentDashboardBinding import foundation.e.advancedprivacy.domain.entities.FeatureState -import foundation.e.advancedprivacy.domain.entities.LocationMode -import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode -import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel.Action import foundation.e.advancedprivacy.features.dashboard.DashboardViewModel.SingleEvent import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel @@ -46,64 +40,89 @@ import org.koin.androidx.viewmodel.ext.android.viewModel class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { private val viewModel: DashboardViewModel by viewModel() - private var graphHolder: GraphHolder? = null - - private var _binding: FragmentDashboardBinding? = null - private val binding get() = _binding!! - - private var highlightIndexOnStart: Int? = null - - private val args: DashboardFragmentArgs by navArgs() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - highlightIndexOnStart = args.highlightLeaks - } + private lateinit var binding: FragmentDashboardBinding override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - _binding = FragmentDashboardBinding.bind(view) - - graphHolder = GraphHolder(binding.graph, requireContext()) - - binding.leakingAppButton.setOnClickListener { - viewModel.submitAction(Action.ShowMostLeakedApp) - } - binding.toggleTrackers.setOnClickListener { - viewModel.submitAction( - Action.ToggleTrackers( - enabled = binding.toggleTrackers.isChecked + binding = FragmentDashboardBinding.bind(view) + + with(binding.dataBlockedTrackers) { + primaryMessage.apply { + setText(R.string.dashboard_data_blocked_trackers_primary) + setCompoundDrawables( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_block_24), + null, + null, + null ) - ) + } + + secondaryMessage.setText(R.string.dashboard_data_blocked_trackers_secondary) } - binding.toggleLocation.setOnClickListener { - viewModel.submitAction( - Action.ToggleLocation( - enabled = binding.toggleLocation.isChecked + with(binding.dataApps) { + primaryMessage.apply { + setText(R.string.dashboard_data_apps_primary) + setCompoundDrawables( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_apps_24), + null, + null, + null ) - ) + } + + secondaryMessage.setText(R.string.dashboard_data_apps_secondary) } - binding.toggleIpscrambling.setOnClickListener { - viewModel.submitAction( - Action.ToggleIpScrambling( - enabled = binding.toggleIpscrambling.isChecked - ) - ) + + binding.trackersControl.title.setText(R.string.dashboard_trackers_title) + binding.fakeLocation.title.setText(R.string.dashboard_location_title) + binding.ipScrambling.title.setText(R.string.dashboard_ipscrambling_title) + + setOnClickListeners() + + listenViewModel() + } + + private fun setOnClickListeners() { + binding.viewTrackersStatistics.setOnClickListener { + viewModel.onClickViewTrackersStatistics() } - binding.myLocation.container.setOnClickListener { - viewModel.submitAction(Action.ShowFakeMyLocationAction) + + with(binding.trackersControl) { + root.setOnClickListener { + viewModel.onClickTrackersControl() + } + + switchFeature.setOnCheckedChangeListener { _, isChecked -> + viewModel.onClickToggleTrackersContol(isChecked) + } } - binding.internetActivityPrivacy.container.setOnClickListener { - viewModel.submitAction(Action.ShowInternetActivityPrivacyAction) + + with(binding.fakeLocation) { + root.setOnClickListener { + viewModel.onClickFakeLocation() + } + + switchFeature.setOnCheckedChangeListener { _, isChecked -> + viewModel.onClickToggleFakeLocation(isChecked) + } } - binding.appsPermissions.container.setOnClickListener { - viewModel.submitAction(Action.ShowAppsPermissions) + + with(binding.ipScrambling) { + root.setOnClickListener { + viewModel.onClickIpScrambling() + } + + switchFeature.setOnCheckedChangeListener { _, isChecked -> + viewModel.onClickToggleIpScrambling(isChecked) + } } - binding.amITracked.container.setOnClickListener { - viewModel.submitAction(Action.ShowTrackers) + binding.appsPermissions.setOnClickListener { + viewModel.onClickAppsPermissions() } + } + private fun listenViewModel() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { render(viewModel.state.value) @@ -125,6 +144,7 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { } } } + viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.navigate.collect(findNavController()::navigate) @@ -144,152 +164,63 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { } private fun render(state: DashboardState) { - binding.stateLabel.text = getString( - when (state.quickPrivacyState) { - QuickPrivacyState.DISABLED -> R.string.dashboard_state_title_off - QuickPrivacyState.FULL_ENABLED -> R.string.dashboard_state_title_on - QuickPrivacyState.ENABLED -> R.string.dashboard_state_title_custom - } - ) - - binding.stateIcon.setImageResource( - if (state.quickPrivacyState.isEnabled()) { - R.drawable.ic_shield_on - } else { - R.drawable.ic_shield_off - } - ) + binding.dataBlockedTrackers.number.text = state.blockedCallsCount.toString() + binding.dataApps.number.text = state.appsWithCallsCount.toString() - binding.toggleTrackers.isChecked = state.trackerMode != TrackerMode.VULNERABLE + with(binding.trackersControl) { + switchFeature.isChecked = state.trackerMode != TrackerMode.VULNERABLE - binding.stateTrackers.text = getString( - when (state.trackerMode) { - TrackerMode.DENIED -> R.string.dashboard_state_trackers_on - TrackerMode.VULNERABLE -> R.string.dashboard_state_trackers_off - TrackerMode.CUSTOM -> R.string.dashboard_state_trackers_custom - } - ) - binding.stateTrackers.setTextColor( - getColor( - requireContext(), - if (state.trackerMode == TrackerMode.VULNERABLE) { - R.color.red_off - } else { - R.color.green_valid + stateLabel.setText( + when (state.trackerMode) { + TrackerMode.DENIED -> R.string.dashboard_state_trackers_on + TrackerMode.VULNERABLE -> R.string.dashboard_state_trackers_off + TrackerMode.CUSTOM -> R.string.dashboard_state_trackers_custom } ) - ) - binding.toggleLocation.isChecked = state.isLocationHidden + stateLabel.setTextColor(getStateColor(state.trackerMode != TrackerMode.VULNERABLE)) + } - binding.stateGeolocation.text = getString( - if (state.isLocationHidden) { - R.string.dashboard_state_geolocation_on - } else { - R.string.dashboard_state_geolocation_off - } - ) - binding.stateGeolocation.setTextColor( - getColor( - requireContext(), + with(binding.fakeLocation) { + switchFeature.isChecked = state.isLocationHidden + stateLabel.setText( if (state.isLocationHidden) { - R.color.green_valid + R.string.dashboard_state_geolocation_on } else { - R.color.red_off + R.string.dashboard_state_geolocation_off } ) - ) + stateLabel.setTextColor(getStateColor(state.isLocationHidden)) + } - binding.toggleIpscrambling.isChecked = state.ipScramblingMode.isChecked - val isLoading = state.ipScramblingMode.isLoading - binding.toggleIpscrambling.isEnabled = ( - state.ipScramblingMode != FeatureState.STOPPING - ) + with(binding.ipScrambling) { + switchFeature.isChecked = state.ipScramblingMode.isChecked - binding.stateIpAddress.text = getString( - if (state.ipScramblingMode == FeatureState.ON) { - R.string.dashboard_state_ipaddress_on - } else { - R.string.dashboard_state_ipaddress_off - } - ) + val isLoading = state.ipScramblingMode.isLoading + switchFeature.isEnabled = (state.ipScramblingMode != FeatureState.STOPPING) - binding.stateIpAddressLoader.visibility = if (isLoading) View.VISIBLE else View.GONE - binding.stateIpAddress.visibility = if (!isLoading) View.VISIBLE else View.GONE + stateLoader.isVisible = isLoading + stateLabel.visibility = if (!isLoading) View.VISIBLE else View.INVISIBLE - binding.stateIpAddress.setTextColor( - getColor( - requireContext(), + stateLabel.setText( if (state.ipScramblingMode == FeatureState.ON) { - R.color.green_valid + R.string.dashboard_state_ipaddress_on } else { - R.color.red_off + R.string.dashboard_state_ipaddress_off } ) - ) - - if (state.dayStatistics?.all { it.first == 0 && it.second == 0 } == true) { - binding.graph.visibility = View.INVISIBLE - binding.graphLegend.isVisible = false - binding.leakingAppButton.isVisible = false - binding.graphEmpty.isVisible = true - } else { - binding.graph.isVisible = true - binding.graphLegend.isVisible = true - binding.leakingAppButton.isVisible = true - binding.graphEmpty.isVisible = false - state.dayStatistics?.let { graphHolder?.data = it } - state.dayLabels?.let { graphHolder?.labels = it } - state.dayGraduations?.let { graphHolder?.graduations = it } - - binding.graphLegend.text = Html.fromHtml( - getString( - R.string.dashboard_graph_trackers_legend, - state.leakedTrackersCount?.toString() ?: "No" - ), - FROM_HTML_MODE_LEGACY - ) - - highlightIndexOnStart?.let { - binding.graph.post { - graphHolder?.highlightIndex(it) - } - highlightIndexOnStart = null - } + stateLabel.setTextColor(getStateColor(state.ipScramblingMode == FeatureState.ON)) } + } - if (state.allowedTrackersCount != null && state.trackersCount != null) { - binding.amITracked.subTitle = getString( - R.string.dashboard_am_i_tracked_subtitle, - state.trackersCount, - state.allowedTrackersCount - ) - } else { - binding.amITracked.subTitle = "" - } - - binding.myLocation.subTitle = getString( - when (state.locationMode) { - LocationMode.REAL_LOCATION -> R.string.dashboard_location_subtitle_off - LocationMode.SPECIFIC_LOCATION -> R.string.dashboard_location_subtitle_specific - LocationMode.RANDOM_LOCATION -> R.string.dashboard_location_subtitle_random - } - ) - - binding.internetActivityPrivacy.subTitle = getString( - if (state.ipScramblingMode == FeatureState.ON) { - R.string.dashboard_internet_activity_privacy_subtitle_on + private fun getStateColor(isActive: Boolean): Int { + return getColor( + requireContext(), + if (isActive) { + R.color.green_valid } else { - R.string.dashboard_internet_activity_privacy_subtitle_off + R.color.red_off } ) - - binding.executePendingBindings() - } - - override fun onDestroyView() { - super.onDestroyView() - graphHolder = null - _binding = null } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt index 774901a8ef4d44d722ca463b965037b5e81280cf..e7b39b6457b46085f5e7810fc72c9b555b15ccd6 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardState.kt @@ -18,7 +18,6 @@ package foundation.e.advancedprivacy.features.dashboard import foundation.e.advancedprivacy.domain.entities.FeatureState -import foundation.e.advancedprivacy.domain.entities.LocationMode import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode @@ -27,11 +26,6 @@ data class DashboardState( val trackerMode: TrackerMode = TrackerMode.VULNERABLE, val isLocationHidden: Boolean = false, val ipScramblingMode: FeatureState = FeatureState.STOPPING, - val locationMode: LocationMode = LocationMode.REAL_LOCATION, - val leakedTrackersCount: Int? = null, - val trackersCount: Int? = null, - val allowedTrackersCount: Int? = null, - val dayStatistics: List>? = null, - val dayLabels: List? = null, - val dayGraduations: List? = null + val blockedCallsCount: Int = 0, + val appsWithCallsCount: Int = 0 ) diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt index a36b24a3bcb24f59f4414806723597e0616cf72e..a820be6576114cb05071c15a6ddd4e9cf53add22 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardViewModel.kt @@ -26,15 +26,12 @@ import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -59,24 +56,20 @@ class DashboardViewModel( suspend fun doOnStartedState() = withContext(Dispatchers.IO) { merge( - getPrivacyStateUseCase.quickPrivacyState.map { - _state.update { s -> s.copy(quickPrivacyState = it) } - }, getPrivacyStateUseCase.ipScramblingMode.map { _state.update { s -> s.copy(ipScramblingMode = it) } }, - trackersStatisticsUseCase.listenUpdates().flatMapLatest { + + trackersStatisticsUseCase.listenUpdates().mapLatest { fetchStatistics() }, + getPrivacyStateUseCase.trackerMode.map { _state.update { s -> s.copy(trackerMode = it) } }, getPrivacyStateUseCase.isLocationHidden.map { _state.update { s -> s.copy(isLocationHidden = it) } }, - getPrivacyStateUseCase.locationMode.map { - _state.update { s -> s.copy(locationMode = it) } - }, getPrivacyStateUseCase.otherVpnRunning.map { _singleEvents.emit( SingleEvent.ToastMessageSingleEvent( @@ -88,52 +81,49 @@ class DashboardViewModel( ).collect {} } - fun submitAction(action: Action) = viewModelScope.launch { - when (action) { - is Action.ToggleTrackers -> { - getPrivacyStateUseCase.toggleTrackers(action.enabled) - // Add delay here to prevent race condition with trackers state. - delay(200) - fetchStatistics().first() - } - is Action.ToggleLocation -> getPrivacyStateUseCase.toggleLocation(action.enabled) - is Action.ToggleIpScrambling -> - getPrivacyStateUseCase.toggleIpScrambling(action.enabled) - is Action.ShowFakeMyLocationAction -> - _navigate.emit(DashboardFragmentDirections.gotoFakeLocationFragment()) - is Action.ShowAppsPermissions -> - _navigate.emit(DashboardFragmentDirections.gotoSettingsPermissionsActivity()) - is Action.ShowInternetActivityPrivacyAction -> - _navigate.emit(DashboardFragmentDirections.gotoInternetPrivacyFragment()) - is Action.ShowTrackers -> - _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) - is Action.ShowMostLeakedApp -> actionShowMostLeakedApp() - } + fun onClickViewTrackersStatistics() = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) } - private suspend fun fetchStatistics(): Flow = withContext(Dispatchers.IO) { - trackersStatisticsUseCase.getNonBlockedTrackersCount().map { nonBlockedTrackersCount -> - trackersStatisticsUseCase.getDayStatistics().let { (dayStatistics, trackersCount) -> - _state.update { s -> - s.copy( - dayStatistics = dayStatistics.callsBlockedNLeaked, - dayLabels = dayStatistics.periods, - dayGraduations = dayStatistics.graduations, - leakedTrackersCount = dayStatistics.trackersCount, - trackersCount = trackersCount, - allowedTrackersCount = nonBlockedTrackersCount - ) - } - } - } + fun onClickTrackersControl() = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoTrackersFragment()) + } + + fun onClickToggleTrackersContol(enabled: Boolean) = viewModelScope.launch(Dispatchers.IO) { + getPrivacyStateUseCase.toggleTrackers(enabled) + } + + fun onClickFakeLocation() = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoFakeLocationFragment()) } - private suspend fun actionShowMostLeakedApp() = withContext(Dispatchers.IO) { - _navigate.emit( - trackersStatisticsUseCase.getMostLeakedApp()?.let { - DashboardFragmentDirections.gotoAppTrackersFragment(appUid = it.uid) - } ?: DashboardFragmentDirections.gotoTrackersFragment() - ) + fun onClickToggleFakeLocation(enabled: Boolean) = viewModelScope.launch(Dispatchers.IO) { + getPrivacyStateUseCase.toggleLocation(enabled) + } + + fun onClickIpScrambling() = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoInternetPrivacyFragment()) + } + + fun onClickToggleIpScrambling(enabled: Boolean) = viewModelScope.launch(Dispatchers.IO) { + getPrivacyStateUseCase.toggleIpScrambling(enabled) + } + + fun onClickAppsPermissions() = viewModelScope.launch { + _navigate.emit(DashboardFragmentDirections.gotoSettingsPermissionsActivity()) + } + + private suspend fun fetchStatistics() = withContext(Dispatchers.IO) { + val blockedCallsCount = trackersStatisticsUseCase.getLastMonthBlockedLeaksCount() + + val appsWithBlockedLeaksCount = trackersStatisticsUseCase.getLastMonthAppsWithBLockedLeaksCount() + + _state.update { + it.copy( + blockedCallsCount = blockedCallsCount, + appsWithCallsCount = appsWithBlockedLeaksCount + ) + } } sealed class SingleEvent { @@ -142,15 +132,4 @@ class DashboardViewModel( val args: List = emptyList() ) : SingleEvent() } - - sealed class Action { - data class ToggleTrackers(val enabled: Boolean) : Action() - data class ToggleLocation(val enabled: Boolean) : Action() - data class ToggleIpScrambling(val enabled: Boolean) : Action() - object ShowFakeMyLocationAction : Action() - object ShowInternetActivityPrivacyAction : Action() - object ShowAppsPermissions : Action() - object ShowTrackers : Action() - object ShowMostLeakedApp : Action() - } } 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 index be7e81bc5521255790a137dd1e07b3cf75e83b06..eb651beef7a3d6def03fa34607a056c0bcaff28d 100644 --- 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 @@ -34,7 +34,7 @@ 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) { +class PeriodMarkerView(context: Context) : MarkerView(context, R.layout.chart_tooltip) { enum class ArrowPosition { LEFT, CENTER, RIGHT } private val arrowMargins = 10.dpToPxF(context) @@ -56,9 +56,6 @@ class PeriodMarkerView(context: Context) : MarkerView(context, R.layout.chart_to 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 @@ -85,7 +82,7 @@ class PeriodMarkerView(context: Context) : MarkerView(context, R.layout.chart_to val span = SpannableStringBuilder(period) span.append(" | ") span.setSpan( - ImageSpan(context, R.drawable.ic_legend_blocked_2, DynamicDrawableSpan.ALIGN_BASELINE), + ImageSpan(context, R.drawable.ic_legend_blocked, DynamicDrawableSpan.ALIGN_BASELINE), span.length - 1, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE diff --git a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt index 5d5a16392f4698cb3c6ae3e0bbb79ccb841b625c..3d58999b28e42b828617651318b70981c20fe377 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/widget/WidgetUI.kt @@ -34,7 +34,6 @@ import foundation.e.advancedprivacy.common.extensions.dpToPxF import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.QuickPrivacyState import foundation.e.advancedprivacy.domain.entities.TrackerMode -import foundation.e.advancedprivacy.features.dashboard.DashboardFragmentArgs import foundation.e.advancedprivacy.main.MainActivity import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_IPSCRAMBLING import foundation.e.advancedprivacy.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_LOCATION @@ -211,12 +210,6 @@ fun render(context: Context, state: State, appWidgetManager: AppWidgetManager) { // leaked (the bar above) val topPadding = graphHeightPx - (blocked + leaked) * ratio setViewPadding(leakedBarIds[index], 0, topPadding.toInt(), 0, 0) - - val highlightPIntent = MainActivity.deepLinkBuilder(context) - .setDestination(R.id.dashboardFragment) - .setArguments(DashboardFragmentArgs(highlightLeaks = index).toBundle()) - .createPendingIntent() - setOnClickPendingIntent(containerBarIds[index], highlightPIntent) } setTextViewText( diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/drawable/ic_apps_24.xml similarity index 61% rename from app/src/main/res/values-night/colors.xml rename to app/src/main/res/drawable/ic_apps_24.xml index 6c1d53c04cc060611b8f3dd6c752551c2ac596cb..7e20536761b0deea124bdb8eca70f136022c074e 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/drawable/ic_apps_24.xml @@ -1,6 +1,6 @@ - - - #169659 - + + + diff --git a/app/src/main/res/drawable/ic_apps_permissions.xml b/app/src/main/res/drawable/ic_apps_permissions.xml deleted file mode 100644 index 5e7a5706deb50e0829b858aa96192bda088aacbf..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_apps_permissions.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/bg_rounded.xml b/app/src/main/res/drawable/ic_block_24.xml similarity index 52% rename from app/src/main/res/drawable/bg_rounded.xml rename to app/src/main/res/drawable/ic_block_24.xml index 0677165648091642d59dd3a41eaf79485dcdd79b..77537b3e58a525f2a18033c3bce930cc3b0c382e 100644 --- a/app/src/main/res/drawable/bg_rounded.xml +++ b/app/src/main/res/drawable/ic_block_24.xml @@ -1,5 +1,6 @@ - - - - - - - - + + + diff --git a/app/src/main/res/drawable/ic_facebook.xml b/app/src/main/res/drawable/ic_facebook.xml deleted file mode 100644 index 49597b3734ad309d20b868899fe230e713d40edf..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_facebook.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_internet_activity.xml b/app/src/main/res/drawable/ic_internet_activity.xml deleted file mode 100644 index 83695ad6a61a2e4976da61575b946951ea2befa2..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_internet_activity.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_my_location.xml b/app/src/main/res/drawable/ic_my_location.xml deleted file mode 100644 index 5d70d16ecb151fe4cc60d7da020923ed80a95744..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_my_location.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_tracked.xml b/app/src/main/res/drawable/ic_tracked.xml deleted file mode 100644 index 6cef537227a9365498de88a9e00e61ff3c9821a6..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_tracked.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/chart_tooltip.xml b/app/src/main/res/layout/chart_tooltip.xml index a88bc6fe2e92e32e23682ea20c38cf96e71dd871..6fabd50dd84e2fea7862bdec7d33500e06105866 100644 --- a/app/src/main/res/layout/chart_tooltip.xml +++ b/app/src/main/res/layout/chart_tooltip.xml @@ -1,4 +1,5 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dashboard_item_submenu_button.xml b/app/src/main/res/layout/dashboard_item_submenu_button.xml index ef03ea329365754848e3ec878e59b45bdda838bb..a4454c2dd52664803f44b86269e5fbc140b56a4b 100644 --- a/app/src/main/res/layout/dashboard_item_submenu_button.xml +++ b/app/src/main/res/layout/dashboard_item_submenu_button.xml @@ -1,4 +1,5 @@ - - - - - - - - + + - + android:layout_width="0dp" - + app:layout_constraintLeft_toRightOf="@+id/switch_feature" + app:layout_constraintRight_toLeftOf="@+id/chevron" - + app:layout_constraintTop_toTopOf="parent" + + app:layout_constraintBottom_toTopOf="@+id/state_label" + app:layout_constraintVertical_chainStyle="packed" + + android:maxLines="1" + + android:layout_marginStart="8dp" + android:layout_marginBottom="8dp" + + android:textColor="@color/primary_text" + android:textSize="14sp" + android:lineHeight="20dp" + android:textFontWeight="400" + android:textAllCaps="true" + tools:text="App trackers" + /> - + + + - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml index b1fdaa02cdf65ac498ff82910649a1cacc0764cc..fd685c92705b2da3b518e92e7a89b05f1369a500 100644 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -18,371 +18,120 @@ android:gravity="center_horizontal" android:layout_height="match_parent" android:layout_width="match_parent" + android:paddingHorizontal="16dp" android:orientation="vertical" > - - + android:layout_height="52dp" + android:layout_marginTop="16dp" + > + - - - + - + - - - - + android:textSize="14sp" + android:lineHeight="24sp" + android:textFontWeight="500" + android:textColor="@color/primary_text" + android:layout_marginBottom="16dp" + android:text="@string/dashboard_settings_title" + /> - - - - - android:visibility="visible"/> - - + + - - - - - - - - - - - - - - + android:maxLines="1" - - - + android:textColor="@color/primary_text" + android:textSize="14sp" + android:lineHeight="20dp" + android:textFontWeight="400" + android:textAllCaps="true" - - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/layout/highlight_data_number.xml b/app/src/main/res/layout/highlight_data_number.xml index 793816546ebce8a285776de34346bd064ab49979..4bb45636969591b236a621622b2051e42c28c138 100644 --- a/app/src/main/res/layout/highlight_data_number.xml +++ b/app/src/main/res/layout/highlight_data_number.xml @@ -32,7 +32,10 @@ android:textFontWeight="400" android:textColor="@color/primary_text" android:textAllCaps="true" + android:drawablePadding="8dp" tools:text="Detected in" + tools:drawableStart="@drawable/ic_shield_alert" + /> - @color/e_switch_track_on @color/e_palette_9 + @color/e_switch_track_on #32F8432E #263238 #FFFFFFFF - #28C97C + + + + #AADCFE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3df052cb5864de8639022b9a69d50cae7c85648e..5177868b47d3b32974e2fe96d7122506d10b7721 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,34 +36,51 @@ @string/app_name - Your online privacy is protected - Your online privacy is unprotected - Custom privacy settings applied - Trackers + + Last 30 days + View Statistics + Blocked + Leaks + From + Apps + Your Privacy Settings + App trackers Vulnerable Denied Custom - Location + Geolocation Exposed - Fake - Real IP address + Protected + Real IP address Exposed Hidden + App permission request + + Your online privacy is protected + Your online privacy is unprotected + Custom privacy settings applied + Trackers + + Location + + Real IP address + Personal data leakage: Today %s trackers have profiled you in the last 24 hours View Manage apps\' trackers %1$d detected trackers, %2$d allowed trackers - Manage apps\' permissions + Manage your permissions - Manage my location Real geolocation Specific fake geolocation Random fake geolocation Manage my Internet address Real IP address exposed Real IP address hidden + + Manage my Internet address Your Internet address or IP address is the identifier assigned to your phone when connected to the Internet.\n\nManage my Internet address enables you to use a fake IP address instead of your real IP address. This way, your Internet activity cannot be linked to your real IP address and to your device. diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt index 34b4e7a863f6ae85a931acaa15efc06a1b1b5d5b..6a5bcef69a4dcf18d187a06ecd8cdcb41f7a502c 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/KoinModule.kt @@ -37,18 +37,10 @@ val trackersModule = module { singleOf(::TrackersRepository) single { - StatsDatabase( - context = androidContext(), - trackersRepository = get() - ) + StatsDatabase(context = androidContext()) } - single { - StatisticsUseCase( - database = get(), - appListsRepository = get() - ) - } + singleOf(::StatisticsUseCase) single { WhitelistRepository( 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 238de4e663ac780c15b5077073d2aac15d925d07..da6ee868454253aba683ca4494ed8035e7e7c9a8 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 @@ -31,7 +31,6 @@ import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry. import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TIMESTAMP import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.COLUMN_NAME_TRACKER import foundation.e.advancedprivacy.trackers.data.StatsDatabase.AppTrackerEntry.TABLE_NAME -import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import java.time.Instant import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -45,8 +44,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber class StatsDatabase( - context: Context, - private val trackersRepository: TrackersRepository + context: Context ) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { @@ -247,75 +245,6 @@ class StatsDatabase( } } - fun getContactedTrackersCount(): Int { - synchronized(lock) { - val db = readableDatabase - var query = "SELECT DISTINCT $COLUMN_NAME_TRACKER FROM $TABLE_NAME" - - val cursor = db.rawQuery(query, arrayOf()) - var count = 0 - while (cursor.moveToNext()) { - trackersRepository.getTracker(cursor.getString(COLUMN_NAME_TRACKER))?.let { - count++ - } - } - cursor.close() - db.close() - return count - } - } - - fun getCalls(appId: String, periodCount: Int, periodUnit: TemporalUnit): Pair { - synchronized(lock) { - val minTimestamp = getPeriodStartTs(periodCount, periodUnit) - val db = readableDatabase - val selection = "$COLUMN_NAME_APPID = ? AND " + - "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + appId, "" + minTimestamp) - val projection = - "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM," + - "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM" - val cursor = db.rawQuery( - "SELECT $projection FROM $TABLE_NAME WHERE $selection", - selectionArg - ) - var calls: Pair = 0 to 0 - if (cursor.moveToNext()) { - val contacted = cursor.getInt(PROJECTION_NAME_CONTACTED_SUM) - val blocked = cursor.getInt(PROJECTION_NAME_BLOCKED_SUM) - calls = blocked to contacted - blocked - } - cursor.close() - db.close() - return calls - } - } - - fun getMostLeakedAppId(periodCount: Int, periodUnit: TemporalUnit): String { - synchronized(lock) { - val minTimestamp = getPeriodStartTs(periodCount, periodUnit) - val db = readableDatabase - val selection = "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + minTimestamp) - val projection = "$COLUMN_NAME_APPID, " + - "SUM($COLUMN_NAME_NUMBER_CONTACTED - $COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_LEAKED_SUM" - val cursor = db.rawQuery( - "SELECT $projection FROM $TABLE_NAME" + - " WHERE $selection" + - " GROUP BY $COLUMN_NAME_APPID" + - " ORDER BY $PROJECTION_NAME_LEAKED_SUM DESC LIMIT 1", - selectionArg - ) - var appId = "" - if (cursor.moveToNext()) { - appId = cursor.getString(COLUMN_NAME_APPID) - } - cursor.close() - db.close() - return appId - } - } - fun getDistinctTrackerAndApp(periodStart: Instant): List> { synchronized(lock) { val db = readableDatabase @@ -332,7 +261,6 @@ class StatsDatabase( null, null, null - ) val res = mutableListOf>() @@ -424,6 +352,49 @@ class StatsDatabase( } } + suspend fun getBlockedLeaksCount(periodCount: Int, periodUnit: TemporalUnit): Int = withContext(Dispatchers.IO) { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ?" + val selectionArg = arrayOf("" + minTimestamp) + val projection = "SUM($COLUMN_NAME_NUMBER_BLOCKED) $PROJECTION_NAME_BLOCKED_SUM" + 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 getAppsWithBLockedLeaksCount(periodCount: Int, periodUnit: TemporalUnit): Int = withContext(Dispatchers.IO) { + synchronized(lock) { + val minTimestamp = getPeriodStartTs(periodCount, periodUnit) + val db = readableDatabase + val selection = "$COLUMN_NAME_TIMESTAMP >= ? AND $COLUMN_NAME_NUMBER_BLOCKED > 0" + val selectionArg = arrayOf("" + minTimestamp) + val cursor = db.rawQuery( + "SELECT COUNT (DISTINCT $COLUMN_NAME_APPID) FROM $TABLE_NAME WHERE $selection", + selectionArg + ) + + var count = 0 + if (cursor.moveToNext()) { + count = cursor.getInt(0) + } + cursor.close() + db.close() + count + } + } + suspend fun logAccess(trackerId: String?, appId: String, blocked: Boolean) = withContext( Dispatchers.IO ) { @@ -528,43 +499,6 @@ class StatsDatabase( } } - suspend fun getTrackers(appIds: List?): List = withContext(Dispatchers.IO) { - synchronized(lock) { - val columns = arrayOf(COLUMN_NAME_TRACKER, COLUMN_NAME_APPID) - var selection: String? = null - - var selectionArg: Array? = null - appIds?.let { appIds -> - selection = "$COLUMN_NAME_APPID IN (${appIds.joinToString(", ") { "'$it'" }})" - selectionArg = arrayOf() - } - - val db = readableDatabase - val cursor = db.query( - true, - TABLE_NAME, - columns, - selection, - selectionArg, - null, - null, - null, - null - ) - val trackers: MutableList = ArrayList() - while (cursor.moveToNext()) { - val trackerId = cursor.getString(COLUMN_NAME_TRACKER) - val tracker = trackersRepository.getTracker(trackerId) - if (tracker != null) { - trackers.add(tracker) - } - } - cursor.close() - db.close() - trackers - } - } - class StatEntry { var appId = "" var sum_contacted = 0 diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt index d629b801380b5cf429895b3552a17bb12d6837ee..a36a6bf1859e07ac130c5fe76b52c048ed0834d2 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/domain/usecases/StatisticsUseCase.kt @@ -18,15 +18,11 @@ package foundation.e.advancedprivacy.trackers.domain.usecases -import foundation.e.advancedprivacy.data.repositories.AppListsRepository -import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.trackers.data.StatsDatabase -import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import java.time.temporal.TemporalUnit class StatisticsUseCase( - private val database: StatsDatabase, - private val appListsRepository: AppListsRepository + private val database: StatsDatabase ) { fun getTrackersCallsOnPeriod(periodsCount: Int, periodUnit: TemporalUnit): List> { return database.getTrackersCallsOnPeriod(periodsCount, periodUnit) @@ -35,26 +31,4 @@ class StatisticsUseCase( fun getActiveTrackersByPeriod(periodsCount: Int, periodUnit: TemporalUnit): Int { return database.getActiveTrackersByPeriod(periodsCount, periodUnit) } - - fun getContactedTrackersCount(): Int { - return database.getContactedTrackersCount() - } - - suspend fun getTrackers(apps: List?): List { - return database.getTrackers(apps?.map { it.apId }) - } - - fun getCalls(app: ApplicationDescription, periodCount: Int, periodUnit: TemporalUnit): Pair { - return database.getCalls(app.apId, periodCount, periodUnit) - } - - fun getMostLeakedApp(periodCount: Int, periodUnit: TemporalUnit): ApplicationDescription? { - return appListsRepository.getApp(database.getMostLeakedAppId(periodCount, periodUnit)) - } - - private fun Map.mapByAppIdToApp(): Map { - return entries.mapNotNull { (apId, value) -> - appListsRepository.getApp(apId)?.let { it to value } - }.toMap() - } }