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

Commit 3ad8c31d authored by Nishith  Khanna's avatar Nishith Khanna
Browse files

Merge branch '3108-report_calls_per_app' into 'main'

3108 report calls per app

See merge request !194
parents 7a454a62 842e568f
Loading
Loading
Loading
Loading
Loading
+73 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.text.style.ReplacementSpan
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import foundation.e.advancedprivacy.common.extensions.dpToPx
import foundation.e.advancedprivacy.common.extensions.dpToPxF

class ChipSpan(
    context: Context,
    paddingHorizontal: Int,
    radius: Int,
    height: Int,
    @ColorRes backgroundColorId: Int,
    @ColorRes textColorId: Int

) : ReplacementSpan() {
    private val paddingPx = paddingHorizontal.dpToPx(context)
    private val chipHeight = height.dpToPxF(context)
    private val radiusPx = radius.dpToPxF(context)

    private val textColor = ContextCompat.getColor(context, textColorId)
    private val backgroundPaint = Paint().apply {
        color = ContextCompat.getColor(context, backgroundColorId)
        style = Paint.Style.FILL
        isAntiAlias = true
    }

    private var size: Int = 0

    override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
        val textRect = Rect()
        paint.getTextBounds(text, start, end, textRect)
        size = textRect.width() + 2 * paddingPx
        return size
    }

    override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
        val top = y + (paint.fontMetrics.ascent + paint.fontMetrics.descent - chipHeight) / 2
        val backgroundRect = RectF(
            x,
            top,
            x + size,
            top + chipHeight
        )
        canvas.drawRoundRect(backgroundRect, radiusPx, radiusPx, backgroundPaint)

        paint.color = textColor
        canvas.drawText(text.slice(start until end).toString(), x + paddingPx, y.toFloat(), paint)
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ class TrackersAndAppsListsUseCase(
    }

    private suspend fun get5MostTrackedAppsLastMonth(): List<AppWithCount> {
        val countByApIds = statsDatabase.getCallsByAppIds(since = Period.MONTH.getPeriodStart().epochSecond)
        val countByApIds = statsDatabase.getCallsByAppIds(start = Period.MONTH.getPeriodStart(), end = Instant.now())

        val countByApps = mutableMapOf<DisplayableApp, Int>()
        countByApIds.forEach { (apId, count) ->
+14 −6
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import foundation.e.advancedprivacy.trackers.data.TrackersRepository
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.random.Random
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -107,9 +109,15 @@ class WeeklyReportUseCase(
        return candidates.map { computeScore(it, history) }.sortedBy { it.score }
    }

    private fun addCallPerAppCandidates(candidates: MutableList<WeeklyReport>, endOfWeek: Instant) {
        val anyApp = appListRepository.displayableApps.value.first().id
        val callsPerWeek = Random.nextInt(10000)
    private suspend fun addCallPerAppCandidates(candidates: MutableList<WeeklyReport>, endOfWeek: Instant) {
        val startOfWeek = getStartOfWeek(endOfWeek)
        val (appIdWithMostCalls, callsPerWeek) = statsDatabase.getCallsByAppIds(startOfWeek, endOfWeek).maxBy { it.value }
        val app = appListRepository.getAppById(appIdWithMostCalls)?.id

        // TODO: get second one on fail ?
        if (app == null) { // appId isn't on the phone anymore, skip.
            return
        }

        val hoursInWeek = 7 * 24
        candidates.add(
@@ -117,7 +125,7 @@ class WeeklyReportUseCase(
                endOfWeek,
                WeeklyReport.StatType.CALLS_PER_APP,
                WeeklyReport.LabelId.CALLS_PER_APP_1,
                anyApp,
                app,
                listOf((callsPerWeek / hoursInWeek).toString())
            )
        )
@@ -127,7 +135,7 @@ class WeeklyReportUseCase(
                endOfWeek,
                WeeklyReport.StatType.CALLS_PER_APP,
                WeeklyReport.LabelId.CALLS_PER_APP_2,
                anyApp,
                app,
                listOf((callsPerWeek / hoursInWeek).toString())
            )
        )
@@ -137,7 +145,7 @@ class WeeklyReportUseCase(
                endOfWeek,
                WeeklyReport.StatType.CALLS_PER_APP,
                WeeklyReport.LabelId.CALLS_PER_APP_3,
                anyApp,
                app,
                listOf((callsPerWeek / 7).toString())
            )
        )
+149 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.features.weeklyreport

import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import foundation.e.advancedprivacy.R
import foundation.e.advancedprivacy.common.ChipSpan
import foundation.e.advancedprivacy.databinding.WeeklyReportItemCallsPerAppBinding
import foundation.e.advancedprivacy.databinding.WeeklyReportItemCallsPerAppForSharingBinding
import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport
import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport

class CallsPerAppViewFactory(private val context: Context) {
    fun getShareTitle(): CharSequence {
        return context.getString(R.string.weeklyreport_share_title_base)
    }

    fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? {
        if (report !is DisplayableReport.ReportWithApp) {
            return null
        }

        val binding = WeeklyReportItemCallsPerAppBinding.inflate(inflater, viewGroup, false)

        binding.appIcon.setImageDrawable(report.app.icon)

        binding.title.text = getTitle(report)
        binding.description.text = buildCallsPerAppDescription(report, false)
        return binding.root
    }

    fun createViewForSharing(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? {
        if (report !is DisplayableReport.ReportWithApp) {
            return null
        }

        val binding = WeeklyReportItemCallsPerAppForSharingBinding.inflate(inflater, viewGroup, false)
        binding.appIcon.setImageDrawable(report.app.icon)
        binding.title.text = getTitle(report)
        binding.description.text = buildCallsPerAppDescription(report, true)
        return binding.root
    }

    fun getTitle(report: DisplayableReport): CharSequence {
        return context.getString(
            when (report.report.labelId) {
                WeeklyReport.LabelId.CALLS_PER_APP_1 ->
                    R.string.weeklyreport_label_calls_per_app_1_title

                WeeklyReport.LabelId.CALLS_PER_APP_2 ->
                    R.string.weeklyreport_label_calls_per_app_2_title

                WeeklyReport.LabelId.CALLS_PER_APP_3 ->
                    R.string.weeklyreport_label_calls_per_app_3_title

                else ->
                    R.string.empty
            }
        )
    }

    fun getDescriptionForNotification(report: DisplayableReport): String {
        if (report !is DisplayableReport.ReportWithApp) {
            return ""
        }
        return buildCallsPerAppDescription(report, forSharing = false).toString()
    }

    fun getIconForNotification(report: DisplayableReport): Drawable? {
        return if (report is DisplayableReport.ReportWithApp) {
            report.app.icon
        } else {
            null
        }
    }

    private fun buildCallsPerAppDescription(report: DisplayableReport.ReportWithApp, forSharing: Boolean = false): CharSequence {
        val labelId = report.report.labelId
        val desc1 = context.getString(
            when (labelId) {
                WeeklyReport.LabelId.CALLS_PER_APP_1 -> R.string.weeklyreport_label_calls_per_app_1_description_1
                WeeklyReport.LabelId.CALLS_PER_APP_2 -> R.string.weeklyreport_label_calls_per_app_2_description_1
                WeeklyReport.LabelId.CALLS_PER_APP_3 -> R.string.weeklyreport_label_calls_per_app_3_description_1
                else -> R.string.empty
            },
            report.app.label
        )

        val counts = report.report.secondaryValues.firstOrNull() ?: ""

        val desc2 = context.getString(
            when {
                labelId == WeeklyReport.LabelId.CALLS_PER_APP_1 && forSharing ->
                    R.string.weeklyreport_label_calls_per_app_1_description_2_sharing
                labelId == WeeklyReport.LabelId.CALLS_PER_APP_1 ->
                    R.string.weeklyreport_label_calls_per_app_1_description_2
                labelId == WeeklyReport.LabelId.CALLS_PER_APP_2 && forSharing ->
                    R.string.weeklyreport_label_calls_per_app_2_description_2_sharing
                labelId == WeeklyReport.LabelId.CALLS_PER_APP_2 ->
                    R.string.weeklyreport_label_calls_per_app_2_description_2
                labelId == WeeklyReport.LabelId.CALLS_PER_APP_3 && forSharing ->
                    R.string.weeklyreport_label_calls_per_app_3_description_2_sharing
                labelId == WeeklyReport.LabelId.CALLS_PER_APP_3 ->
                    R.string.weeklyreport_label_calls_per_app_3_description_2
                else -> R.string.empty
            }
        )

        return buildSpannedString {
            append(desc1)
            append(" ")
            inSpans(
                ChipSpan(
                    context = context,
                    paddingHorizontal = 8,
                    radius = 12,
                    height = 16,
                    backgroundColorId = if (forSharing) R.color.share_blue_highlight_number else R.color.divider,
                    textColorId = if (forSharing) R.color.white else R.color.secondary_text
                )
            ) {
                append(counts)
            }
            append(" ")
            append(desc2)
        }
    }
}
+16 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport
class WeeklyReportViewFactory(private val context: Context) {
    val newTrackersViewFactory: NewTrackersViewFactory by lazy { NewTrackersViewFactory(context) }
    val trackerWithMostAppsViewFactory: TrackerWithMostAppsViewFactory by lazy { TrackerWithMostAppsViewFactory(context) }
    val callsPerAppViewFactory: CallsPerAppViewFactory by lazy { CallsPerAppViewFactory(context) }

    fun getShareTitle(report: DisplayableReport): CharSequence {
        return when (report.report.statType) {
@@ -45,6 +46,8 @@ class WeeklyReportViewFactory(private val context: Context) {
                newTrackersViewFactory.getShareTitle(report)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getShareTitle()
            WeeklyReport.StatType.CALLS_PER_APP ->
                callsPerAppViewFactory.getShareTitle()
            else -> context.getString(R.string.weeklyreport_share_title_base)
        }
    }
@@ -55,6 +58,8 @@ class WeeklyReportViewFactory(private val context: Context) {
                newTrackersViewFactory.createView(report, inflater, viewGroup, onClick)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.createView(report, inflater, viewGroup, onClick)
            WeeklyReport.StatType.CALLS_PER_APP ->
                callsPerAppViewFactory.createView(report, inflater, viewGroup)
            else -> null
        } ?: createDefaultView(report, inflater, viewGroup)
    }
@@ -109,6 +114,9 @@ class WeeklyReportViewFactory(private val context: Context) {
                newTrackersViewFactory.createViewForSharing(report, inflater, viewGroup)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.createViewForSharing(report, inflater, viewGroup)
            WeeklyReport.StatType.CALLS_PER_APP ->
                callsPerAppViewFactory.createViewForSharing(report, inflater, viewGroup)

            else -> null
        } ?: createDefaultView(report, inflater, viewGroup)
    }
@@ -128,6 +136,9 @@ class WeeklyReportViewFactory(private val context: Context) {
                newTrackersViewFactory.getTitle(report)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getTitle()
            WeeklyReport.StatType.CALLS_PER_APP ->
                callsPerAppViewFactory.getTitle(report)

            else -> report.report.labelId.name
        }
    }
@@ -138,6 +149,9 @@ class WeeklyReportViewFactory(private val context: Context) {
                newTrackersViewFactory.getDescriptionForNotification(report)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getDescriptionForNotification(report)
            WeeklyReport.StatType.CALLS_PER_APP ->
                callsPerAppViewFactory.getDescriptionForNotification(report)

            else -> getDefaultDescription(report).toString()
        }
    }
@@ -158,6 +172,8 @@ class WeeklyReportViewFactory(private val context: Context) {
                newTrackersViewFactory.getIconForNotification(report)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getIconForNotification()
            WeeklyReport.StatType.CALLS_PER_APP ->
                callsPerAppViewFactory.getIconForNotification(report)
            else -> null
        }
    }
Loading