Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b31bad0e authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

Merge branch '1724-update_home_ui' into 'main'

1724 update home ux

See merge request !157
parents 41bd9eb2 4fcaa1f6
Loading
Loading
Loading
Loading
Loading
+0 −352
Original line number Original line Diff line number Diff line
/*
 * 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 <https://www.gnu.org/licenses/>.
 */

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<Pair<Int, Int>>()
        set(value) {
            field = value
            refreshDataSet()
        }
    var labels = emptyList<String>()

    var graduations: List<String?>? = 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<View>(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<TextView>(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)
    }
}
+6 −46
Original line number Original line Diff line number Diff line
@@ -35,8 +35,6 @@ import java.time.temporal.ChronoUnit
import kotlin.time.Duration
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.onStart


@@ -57,38 +55,6 @@ class TrackersStatisticsUseCase(
        .throttleFirst(windowDuration = debounce)
        .throttleFirst(windowDuration = debounce)
        .onStart { emit(Unit) }
        .onStart { emit(Unit) }


    fun getDayStatistics(): Pair<TrackersPeriodicStatistics, Int> {
        return TrackersPeriodicStatistics(
            callsBlockedNLeaked = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS),
            periods = buildDayLabels(),
            trackersCount = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS),
            graduations = buildDayGraduations()
        ) to statisticsUseCase.getContactedTrackersCount()
    }

    fun getNonBlockedTrackersCount(): Flow<Int> {
        return if (whitelistRepository.isBlockingEnabled) {
            appListsRepository.allApps().map { apps ->
                val whiteListedTrackers = mutableSetOf<Tracker>()
                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 getDayTrackersCalls() = statisticsUseCase.getTrackersCallsOnPeriod(24, ChronoUnit.HOURS)


    fun getDayTrackersCount() = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS)
    fun getDayTrackersCount() = statisticsUseCase.getActiveTrackersByPeriod(24, ChronoUnit.HOURS)
@@ -211,18 +177,12 @@ class TrackersStatisticsUseCase(
        )
        )
    }
    }


    suspend fun getCalls(app: ApplicationDescription): Pair<Int, Int> {
    suspend fun getLastMonthBlockedLeaksCount(): Int {
        return appListsRepository.mapReduceForHiddenApps(
        return statsDatabase.getBlockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit)
            app = app,
            map = {
                statisticsUseCase.getCalls(it, 24, ChronoUnit.HOURS)
            },
            reduce = { zip ->
                zip.unzip().let { (blocked, leaked) ->
                    blocked.sum() to leaked.sum()
                }
    }
    }
        )

    suspend fun getLastMonthAppsWithBLockedLeaksCount(): Int {
        return statsDatabase.getAppsWithBLockedLeaksCount(Period.MONTH.periodsCount, Period.MONTH.periodUnit)
    }
    }


    private fun getWhiteList(app: ApplicationDescription): List<Tracker> {
    private fun getWhiteList(app: ApplicationDescription): List<Tracker> {
Loading