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

Commit 6f6f6401 authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

feat:3064: Add Big brother is watching you report.

parent 10929e1d
Loading
Loading
Loading
Loading
+17 −6
Original line number Diff line number Diff line
@@ -27,10 +27,15 @@ import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase
import foundation.e.advancedprivacy.domain.usecases.TrackersStateUseCase
import foundation.e.advancedprivacy.domain.usecases.TrackersStatisticsUseCase
import foundation.e.advancedprivacy.domain.usecases.VpnSupervisorUseCase
import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase
import foundation.e.advancedprivacy.externalinterfaces.workers.WeeklyReportWorker
import foundation.e.advancedprivacy.trackers.data.TrackersRepository
import foundation.e.advancedprivacy.trackers.services.UpdateTrackersWorker
import foundation.e.lib.telemetry.Telemetry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.java.KoinJavaComponent.get
@@ -44,21 +49,26 @@ class AdvancedPrivacyApplication : Application() {
            androidContext(this@AdvancedPrivacyApplication)
            modules(appModule)
        }

        get<CoroutineScope>(CoroutineScope::class.java).launch {
            initBackgroundSingletons()
        }
    }

    private suspend fun initBackgroundSingletons() = withContext(Dispatchers.IO) {
        get<TrackersRepository>(TrackersRepository::class.java).initTrackersFile()

    private fun initBackgroundSingletons() {
        UpdateTrackersWorker.periodicUpdate(this)
        WeeklyReportWorker.scheduleNext(this)
        UpdateTrackersWorker.periodicUpdate(this@AdvancedPrivacyApplication)
        WeeklyReportWorker.scheduleNext(this@AdvancedPrivacyApplication)

        WarningDialog.startListening(
            get(ShowFeaturesWarningUseCase::class.java),
            get(CoroutineScope::class.java),
            this
            this@AdvancedPrivacyApplication
        )

        Widget.startListening(
            this,
            this@AdvancedPrivacyApplication,
            get(GetQuickPrivacyStateUseCase::class.java),
            get(TrackersStatisticsUseCase::class.java)
        )
@@ -69,5 +79,6 @@ class AdvancedPrivacyApplication : Application() {
        get<TrackersStateUseCase>(TrackersStateUseCase::class.java)
        get<FakeLocationStateUseCase>(FakeLocationStateUseCase::class.java)
        get<VpnSupervisorUseCase>(VpnSupervisorUseCase::class.java).listenSettings()
        get<WeeklyReportUseCase>(WeeklyReportUseCase::class.java).listen()
    }
}
+41 −18
Original line number Diff line number Diff line
@@ -51,7 +51,7 @@ class WeeklyReportUseCase(

    private var stopDisplayJob: Job? = null

    init {
    fun listen() {
        scope.launch {
            updateCurrent()
        }
@@ -66,7 +66,6 @@ class WeeklyReportUseCase(
            stopDisplayJob = scope.launch {
                delay(endOfDisplay.toEpochMilli() - now.toEpochMilli())
                _currentReport.value = null

                stopDisplayJob = null
            }
        } else {
@@ -104,7 +103,6 @@ class WeeklyReportUseCase(
        addCallPerAppCandidates(candidates, endOfWeek)
        addNewTrackerCandidates(candidates, endOfWeek)
        addCallAndLeaksCandidates(candidates, endOfWeek)
        addTrackerWithMostAppsCandidates(candidates, endOfWeek)

        return candidates.map { computeScore(it, history) }.sortedBy { it.score }
    }
@@ -145,8 +143,8 @@ class WeeklyReportUseCase(
        )
    }

    private suspend fun addNewTrackerCandidates(candidates: MutableList<WeeklyReport>, endOfWeek: Instant) {
        val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS)
    private suspend fun addNewTrackerCandidates(candidates: MutableList<WeeklyReport>, endOfWeek: Instant) = withContext(Dispatchers.IO) {
        val startOfWeek = getStartOfWeek(endOfWeek)
        val startOfYear = endOfWeek.minus(365, ChronoUnit.DAYS)

        val trackerAppsHistoric = statsDatabase.getDistinctTrackerAndApp(startOfYear, startOfWeek)
@@ -156,7 +154,6 @@ class WeeklyReportUseCase(
        val newTrackersDetected = trackerAppsOfWeek.filter { it.first !in historicTrackers }

        val appsIntroducingNewTracker = newTrackersDetected.mapNotNull { appListRepository.getAppById(it.second) }.toSet()
        // TODO dummy apps ?&& displayableApp != appListRepository
        appsIntroducingNewTracker.forEach { app ->
            candidates.add(
                WeeklyReport(
@@ -193,6 +190,44 @@ class WeeklyReportUseCase(
                )
            )
        }

        addTrackerWithMostAppsCandidates(
            candidates,
            endOfWeek,
            trackerAppsHistoric.toSet() + trackerAppsOfWeek.toSet()
        )
    }

    private fun addTrackerWithMostAppsCandidates(
        candidates: MutableList<WeeklyReport>,
        endOfWeek: Instant,
        trackerApps: Set<Pair<String, String>>
    ) {
        val (tracker, appCount) = trackerApps.groupBy(
            keySelector = { (trackerId, _) ->
                trackersRepository.getTracker(trackerId)
            },
            valueTransform = { (_, apId) ->
                appListRepository.getInternetAppByApId(apId)
            }
        ).filterKeys { it != null }
            .mapValues { appsByTracker -> appsByTracker.value.filterNotNull().distinct().size }
            .maxBy { appCountByTracker -> appCountByTracker.value }
            .let { appCountByTracker ->
                appCountByTracker.key to appCountByTracker.value
            }

        if (tracker != null && appCount > 1) {
            candidates.add(
                WeeklyReport(
                    endOfWeek,
                    WeeklyReport.StatType.TRACKER_WITH_MOST_APPS,
                    WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1,
                    tracker.id,
                    listOf(appCount.toString())
                )
            )
        }
    }

    private fun addCallAndLeaksCandidates(candidates: MutableList<WeeklyReport>, endOfWeek: Instant) {
@@ -219,18 +254,6 @@ class WeeklyReportUseCase(
        )
    }

    private fun addTrackerWithMostAppsCandidates(candidates: MutableList<WeeklyReport>, endOfWeek: Instant) {
        candidates.add(
            WeeklyReport(
                endOfWeek,
                WeeklyReport.StatType.TRACKER_WITH_MOST_APPS,
                WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1,
                "wtm_yahoo",
                listOf(Random.nextInt(20).toString())
            )
        )
    }

    private fun computeScore(weeklyReport: WeeklyReport, history: List<WeeklyReport>): WeeklyReportScore {
        // Smaller score is the best to display.
        // Score is the rank in history, categorized by most visible parameters:
+8 −3
Original line number Diff line number Diff line
@@ -105,16 +105,21 @@ class TrackersViewModel(

    fun onClickWeeklyReportAction() = viewModelScope.launch {
        val report = state.value
        val labelId = report?.report?.labelId
        when {
            report?.report?.labelId == WeeklyReport.LabelId.NEW_TRACKER_1 &&
            labelId == WeeklyReport.LabelId.NEW_TRACKER_1 &&
                report is DisplayableReport.ReportWithApp -> {
                _navigate.emit(TrackersFragmentDirections.gotoAppTrackersFragment(appId = report.app.id))
            }

            report?.report?.labelId == WeeklyReport.LabelId.NEW_TRACKER_2 ||
                report?.report?.labelId == WeeklyReport.LabelId.NEW_TRACKER_3 -> {
            labelId == WeeklyReport.LabelId.NEW_TRACKER_2 ||
                labelId == WeeklyReport.LabelId.NEW_TRACKER_3 -> {
                trackersScreenUseCase.selectTab(Period.MONTH, TrackerTab.TRACKERS)
            }

            labelId == WeeklyReport.LabelId.TRACKER_WITH_MOST_APPS_1 &&
                report is DisplayableReport.ReportWithTracker ->
                _navigate.emit(TrackersFragmentDirections.gotoTrackerDetailsFragment(trackerId = report.tracker.id))
        }
    }

+121 −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.content.res.Configuration
import android.graphics.drawable.Drawable
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import foundation.e.advancedprivacy.R
import foundation.e.advancedprivacy.databinding.WeeklyreportItemNewTrackersBinding
import foundation.e.advancedprivacy.databinding.WeeklyreportItemNewTrackersForSharingBinding
import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport

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

    fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup, onClick: () -> Unit): View? {
        val binding = WeeklyreportItemNewTrackersBinding.inflate(inflater, viewGroup, false)

        if (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) {
            binding.icon.setImageResource(R.drawable.ic_big_brother_yellow_bg)
        } else {
            binding.icon.setImageResource(R.drawable.ic_big_brother)
        }
        binding.icon.imageTintList = null

        binding.title.text = getTitle()

        binding.description.text = buildNewTrackersDescription(report, onClick)
        binding.description.movementMethod = LinkMovementMethod.getInstance()
        with(binding.viewDetails) {
            setText(R.string.weeklyreport_label_tracker_with_most_apps_1_cta)
            setOnClickListener { onClick() }
        }

        return binding.root
    }

    fun createViewForSharing(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? {
        val binding = WeeklyreportItemNewTrackersForSharingBinding.inflate(inflater, viewGroup, false)

        binding.icon.setImageDrawable(getIconForNotification())
        binding.title.text = getTitle()
        binding.description.text = buildNewTrackersDescription(report, null, true)

        return binding.root
    }

    fun getTitle(): CharSequence {
        return context.getString(R.string.weeklyreport_label_tracker_with_most_apps_1_title)
    }

    fun getDescriptionForNotification(report: DisplayableReport): String {
        return buildNewTrackersDescription(report, null, forSharing = false).toString()
    }

    fun getIconForNotification(): Drawable? {
        return ContextCompat.getDrawable(context, R.drawable.ic_big_brother_yellow_bg)
    }

    private fun buildNewTrackersDescription(
        report: DisplayableReport,
        clickCallBack: (() -> Unit)?,
        forSharing: Boolean = false
    ): CharSequence {
        val value = (report as? DisplayableReport.ReportWithTracker)?.tracker?.label ?: ""
        return buildSpannedString {
            if (forSharing) {
                bold {
                    append(value)
                }
            } else {
                inSpans(
                    object : ClickableSpan() {
                        override fun onClick(p0: View) {
                            clickCallBack?.invoke()
                        }
                    }
                ) {
                    append(value)
                }
            }
            append(" ")
            append(
                context.getString(
                    if (forSharing) {
                        R.string.weeklyreport_label_tracker_with_most_apps_1_description_sharing
                    } else {
                        R.string.weeklyreport_label_tracker_with_most_apps_1_description
                    },
                    report.report.secondaryValues.firstOrNull() ?: ""
                )
            )
        }
    }
}
+13 −5
Original line number Diff line number Diff line
@@ -37,12 +37,14 @@ 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) }

    fun getShareTitle(report: DisplayableReport): CharSequence {
        return when (report.report.statType) {
            WeeklyReport.StatType.NEW_TRACKER ->
                newTrackersViewFactory.getShareTitle(report)

            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getShareTitle()
            else -> context.getString(R.string.weeklyreport_share_title_base)
        }
    }
@@ -51,7 +53,8 @@ class WeeklyReportViewFactory(private val context: Context) {
        return when (report.report.statType) {
            WeeklyReport.StatType.NEW_TRACKER ->
                newTrackersViewFactory.createView(report, inflater, viewGroup, onClick)

            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.createView(report, inflater, viewGroup, onClick)
            else -> null
        } ?: createDefaultView(report, inflater, viewGroup)
    }
@@ -104,7 +107,8 @@ class WeeklyReportViewFactory(private val context: Context) {
        return when (report.report.statType) {
            WeeklyReport.StatType.NEW_TRACKER ->
                newTrackersViewFactory.createViewForSharing(report, inflater, viewGroup)

            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.createViewForSharing(report, inflater, viewGroup)
            else -> null
        } ?: createDefaultView(report, inflater, viewGroup)
    }
@@ -122,7 +126,8 @@ class WeeklyReportViewFactory(private val context: Context) {
        return when (report.report.statType) {
            WeeklyReport.StatType.NEW_TRACKER ->
                newTrackersViewFactory.getTitle(report)

            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getTitle()
            else -> report.report.labelId.name
        }
    }
@@ -131,7 +136,8 @@ class WeeklyReportViewFactory(private val context: Context) {
        return when (report.report.statType) {
            WeeklyReport.StatType.NEW_TRACKER ->
                newTrackersViewFactory.getDescriptionForNotification(report)

            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getDescriptionForNotification(report)
            else -> getDefaultDescription(report).toString()
        }
    }
@@ -150,6 +156,8 @@ class WeeklyReportViewFactory(private val context: Context) {
        return when (report.report.statType) {
            WeeklyReport.StatType.NEW_TRACKER ->
                newTrackersViewFactory.getIconForNotification(report)
            WeeklyReport.StatType.TRACKER_WITH_MOST_APPS ->
                trackerWithMostAppsViewFactory.getIconForNotification()
            else -> null
        }
    }
Loading