diff --git a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt index 670d7be937be35117651f70a9b8c0673cb88df1f..7b7353b8d8d9e70938e16d2adb2eb35fb0be1629 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt @@ -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) } - initBackgroundSingletons() + + get(CoroutineScope::class.java).launch { + initBackgroundSingletons() + } } - private fun initBackgroundSingletons() { - UpdateTrackersWorker.periodicUpdate(this) - WeeklyReportWorker.scheduleNext(this) + private suspend fun initBackgroundSingletons() = withContext(Dispatchers.IO) { + get(TrackersRepository::class.java).initTrackersFile() + + 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::class.java) get(FakeLocationStateUseCase::class.java) get(VpnSupervisorUseCase::class.java).listenSettings() + get(WeeklyReportUseCase::class.java).listen() } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt index 46338179d23c19bb4abf00ca8a87d1452f84aa75..08eff4168d7a6b21ef5be7d7c125d2c7cce679ff 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/WeeklyReportUseCase.kt @@ -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, endOfWeek: Instant) { - val startOfWeek = endOfWeek.minus(7, ChronoUnit.DAYS) + private suspend fun addNewTrackerCandidates(candidates: MutableList, 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, + endOfWeek: Instant, + trackerApps: Set> + ) { + 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, endOfWeek: Instant) { @@ -219,18 +254,6 @@ class WeeklyReportUseCase( ) } - private fun addTrackerWithMostAppsCandidates(candidates: MutableList, 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): WeeklyReportScore { // Smaller score is the best to display. // Score is the rank in history, categorized by most visible parameters: diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt index 1b2a16bebb492e33570c28899013a666db551ba8..47ed50a9616aa228d756b25eaee90ea55e102456 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersViewModel.kt @@ -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)) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/TrackerWithMostAppsViewFactory.kt b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/TrackerWithMostAppsViewFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..af34935e378e4f10c313c12f49af35367cc9f314 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/TrackerWithMostAppsViewFactory.kt @@ -0,0 +1,121 @@ +/* + * 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 . + */ + +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() ?: "" + ) + ) + } + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt index fb01781bf7bda7a059600ef23455730291fe9267..9e9857e1342343215d3afe5b220b720ff83ad95f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt @@ -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 } } diff --git a/app/src/main/res/drawable/ic_big_brother.xml b/app/src/main/res/drawable/ic_big_brother.xml new file mode 100644 index 0000000000000000000000000000000000000000..12dc93a948792a8858c6b446f46298da5063cc67 --- /dev/null +++ b/app/src/main/res/drawable/ic_big_brother.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_big_brother_yellow_bg.xml b/app/src/main/res/drawable/ic_big_brother_yellow_bg.xml new file mode 100644 index 0000000000000000000000000000000000000000..ddb7355d15d843fef1e940dff0a54535e8ce11ce --- /dev/null +++ b/app/src/main/res/drawable/ic_big_brother_yellow_bg.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1e4af532745811fc81b2e2534c8ea55909ca4353..111f3cc706c04debbaf060072224d33712cfa4ff 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -180,6 +180,11 @@ haben diese Woche neue versteckte Tracker auf meinem Smartphone hinzugefügt. Alle ansehen + Big Brother beobachtet dich! + ist in %s deiner Apps vorhanden. + ist in %s Apps auf meinem Telefon vorhanden. + Apps ansehen + Wöchentlicher Bericht Eine wöchentliche Benachrichtigung, die eine Zusammenfassung der Tracker zeigt und Änderungen sowie Updates hervorhebt. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index baef26fc751889f69997c8f59b27bd87df159978..92c48eea74a94fb0f67ecaa39fa1b8768d3c8b40 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -180,6 +180,11 @@ añadieron nuevos rastreadores ocultos esta semana en mi smartphone. Ver todos + ¡Gran Hermano te está vigilando! + está presente en %s de tus aplicaciones. + está presente en %s aplicaciones en mi teléfono. + Ver aplicaciones + Informe Semanal Una notificación semanal que muestra un resumen de los rastreadores, destacando cualquier cambio y actualización en ellos. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f554af56c38ba6de238c33a2de21995749672706..6e31124436c0272d67db9432641853a498d72154 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -178,6 +178,11 @@ ont ajouté de nouveaux pisteurs cachés cette semaine sur mon smartphone. Voir tous + Big Brother te surveille ! + est présent dans %s de vos applications. + est présent dans %s applications sur mon téléphone. + Voir les applications + Rapport Hebdomadaire Une notification hebdomadaire présentant un résumé des pisteurs, mettant en évidence les changements et mises à jour. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3f033800617a4e9c024e471d0fe7f00cab0320b8..f9ef4a3096e771801acc9b65d2c51d6229cd1442 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -181,6 +181,11 @@ hanno aggiunto nuovi tracker nascosti questa settimana sul mio smartphone. Visualizza tutti + Il Grande Fratello ti sta osservando! + è presente in %s delle tue app. + è presente in %s app sul mio telefono. + Visualizza app + Rapporto Settimanale Una notifica settimanale che mostra un riepilogo dei tracker, evidenziando eventuali modifiche e aggiornamenti. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63c32c480fd5f218e0c2e12741f6be76b71dd18d..7df349f37fa26310995c9e1684e7d0e4248985bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,6 +213,11 @@ added new hidden trackers this week on my smartphone. View all + Big Brother is watching you! + is present in %s of your apps. + is present in %s apps on my phone. + View apps + @string/app_name diff --git a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt index 858de13e0996130815ea1d24eda6a942fd56a1b3..01234572e67eef79552690f2addff15e0800737f 100644 --- a/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt +++ b/trackers/src/main/java/foundation/e/advancedprivacy/trackers/data/TrackersRepository.kt @@ -21,9 +21,6 @@ import android.content.Context import foundation.e.advancedprivacy.trackers.domain.entities.Tracker import java.io.File import java.io.FileInputStream -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -31,8 +28,7 @@ import timber.log.Timber class TrackersRepository( private val context: Context, - private val json: Json, - coroutineScope: CoroutineScope + private val json: Json ) { private var trackersById: Map = HashMap() @@ -41,12 +37,6 @@ class TrackersRepository( private val eTrackerFileName = "e_trackers.json" val eTrackerFile = File(context.filesDir.absolutePath, eTrackerFileName) - init { - coroutineScope.launch(Dispatchers.IO) { - initTrackersFile() - } - } - @OptIn(ExperimentalSerializationApi::class) fun initTrackersFile() { try {