From 50f3232a17a248b3cc1f2696dcae14fe00bb8aef Mon Sep 17 00:00:00 2001 From: jacquarg Date: Wed, 2 Apr 2025 11:19:02 +0200 Subject: [PATCH 1/2] feat:3108: computation and UI for calls per app reports --- .../e/advancedprivacy/common/ChipSpan.kt | 73 +++++++++ .../usecases/TrackersAndAppsListsUseCase.kt | 2 +- .../domain/usecases/WeeklyReportUseCase.kt | 20 ++- .../weeklyreport/CallsPerAppViewFactory.kt | 149 ++++++++++++++++++ .../weeklyreport/WeeklyReportViewFactory.kt | 16 ++ app/src/main/res/layout/fragment_trackers.xml | 5 +- .../weekly_report_item_calls_per_app.xml | 66 ++++++++ ..._report_item_calls_per_app_for_sharing.xml | 65 ++++++++ app/src/main/res/values/strings.xml | 16 +- .../trackers/data/StatsDatabase.kt | 6 +- 10 files changed, 406 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/foundation/e/advancedprivacy/common/ChipSpan.kt create mode 100644 app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/CallsPerAppViewFactory.kt create mode 100644 app/src/main/res/layout/weekly_report_item_calls_per_app.xml create mode 100644 app/src/main/res/layout/weekly_report_item_calls_per_app_for_sharing.xml diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ChipSpan.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ChipSpan.kt new file mode 100644 index 00000000..19cd47d8 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/common/ChipSpan.kt @@ -0,0 +1,73 @@ +/* + * 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.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) + } +} diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt index c12a6cde..19ddc852 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/TrackersAndAppsListsUseCase.kt @@ -60,7 +60,7 @@ class TrackersAndAppsListsUseCase( } private suspend fun get5MostTrackedAppsLastMonth(): List { - val countByApIds = statsDatabase.getCallsByAppIds(since = Period.MONTH.getPeriodStart().epochSecond) + val countByApIds = statsDatabase.getCallsByAppIds(start = Period.MONTH.getPeriodStart(), end = Instant.now()) val countByApps = mutableMapOf() countByApIds.forEach { (apId, count) -> 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 08eff416..c61b7bad 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 @@ -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, endOfWeek: Instant) { - val anyApp = appListRepository.displayableApps.value.first().id - val callsPerWeek = Random.nextInt(10000) + private suspend fun addCallPerAppCandidates(candidates: MutableList, 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()) ) ) diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/CallsPerAppViewFactory.kt b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/CallsPerAppViewFactory.kt new file mode 100644 index 00000000..6f21a407 --- /dev/null +++ b/app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/CallsPerAppViewFactory.kt @@ -0,0 +1,149 @@ +/* + * 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.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) + } + } +} 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 9e9857e1..25fe0d85 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 @@ -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 } } diff --git a/app/src/main/res/layout/fragment_trackers.xml b/app/src/main/res/layout/fragment_trackers.xml index 8d0cb39f..c14164e4 100644 --- a/app/src/main/res/layout/fragment_trackers.xml +++ b/app/src/main/res/layout/fragment_trackers.xml @@ -62,6 +62,10 @@ android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" android:text="@string/weeklyreport_section_title" + android:layout_marginTop="24dp" + android:textFontWeight="500" + android:textSize="14sp" + android:lineHeight="24dp" /> + + + > + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/weekly_report_item_calls_per_app_for_sharing.xml b/app/src/main/res/layout/weekly_report_item_calls_per_app_for_sharing.xml new file mode 100644 index 00000000..8438893d --- /dev/null +++ b/app/src/main/res/layout/weekly_report_item_calls_per_app_for_sharing.xml @@ -0,0 +1,65 @@ + + + + > + + + + \ 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 7df349f3..989ca8fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -218,6 +218,21 @@ is present in %s apps on my phone. View apps + Top Privacy Offender: + + %s made + tracking attempts per hour this week. + tracking attempts per hour this week on my smartphone. + + Most Invasive App: + %s leaked data + times per hour this week. + times per hour this week on my smartphone. + Biggest Data Leaker: + %s sent data + times per day this week. + times per day this week on my smartphone. + @string/app_name @@ -250,7 +265,6 @@ Highlight that the trackers are actually logged and blocked by Advanced Privacy Tracker control is on This could impact the functioning of some applications. - Weekly Report A weekly notification showing a summary of the trackers, highlighting any changes and updates in them. 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 49b9f4c1..7c6222c5 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 @@ -371,11 +371,11 @@ class StatsDatabase( } } - suspend fun getCallsByAppIds(since: Long): Map = withContext(Dispatchers.IO) { + suspend fun getCallsByAppIds(start: Instant, end: Instant): Map = withContext(Dispatchers.IO) { synchronized(lock) { val db = readableDatabase - val selection = "$COLUMN_NAME_TIMESTAMP >= ?" - val selectionArg = arrayOf("" + since) + val selection = "$COLUMN_NAME_TIMESTAMP >= ? AND $COLUMN_NAME_TIMESTAMP <= ?" + val selectionArg = arrayOf("" + start.epochSecond, "" + end.epochSecond) val projection = "$COLUMN_NAME_APPID, " + "SUM($COLUMN_NAME_NUMBER_CONTACTED) $PROJECTION_NAME_CONTACTED_SUM" val cursor = db.rawQuery( -- GitLab From 842e568f994b6e79e855ee96dbc4964a894f4cd0 Mon Sep 17 00:00:00 2001 From: jacquarg Date: Wed, 2 Apr 2025 11:36:59 +0200 Subject: [PATCH 2/2] feat:3108: translate in de, es, fr, it with IA help. --- app/src/main/res/values-de/strings.xml | 16 ++++++++++++++++ app/src/main/res/values-es/strings.xml | 19 +++++++++++++++++-- app/src/main/res/values-fr/strings.xml | 17 +++++++++++++++++ app/src/main/res/values-it/strings.xml | 17 +++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 111f3cc7..3ca581e1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -187,4 +187,20 @@ Wöchentlicher Bericht Eine wöchentliche Benachrichtigung, die eine Zusammenfassung der Tracker zeigt und Änderungen sowie Updates hervorhebt. + + Top-Datenschutz-Verletzer: + + %s machte + Tracking-Versuche pro Stunde in dieser Woche. + Tracking-Versuche pro Stunde in dieser Woche auf meinem Smartphone. + + Invasivste App: + %s hat Datenlecks + mal pro Stunde in dieser Woche. + mal pro Stunde in dieser Woche auf meinem Smartphone. + + Größter Datenlecker: + %s hat Daten gesendet + mal pro Tag in dieser Woche. + mal pro Tag in dieser Woche auf meinem Smartphone. \ 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 92c48eea..d040f70f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -160,8 +160,6 @@ Informe semanal: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_ES.pdf - ¿Impresionado? - ¡Corre la voz! ¡Nuevo espía en la ciudad! Esta semana @@ -187,4 +185,21 @@ Informe Semanal Una notificación semanal que muestra un resumen de los rastreadores, destacando cualquier cambio y actualización en ellos. + + ¿Impresionado? + Principal infractor de privacidad: + %s ha realizado + intentos de seguimiento por hora esta semana. + intentos de seguimiento por hora esta semana en mi smartphone. + + Aplicación más invasiva: + %s ha intentado filtrar datos + veces por hora esta semana. + veces por hora esta semana en mi smartphone. + + Mayor filtrador de datos: + %s ha intentado enviar datos + veces por día esta semana. + veces por día esta semana en mi smartphone. + \ 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 6e311244..817a2c5d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -185,4 +185,21 @@ Rapport Hebdomadaire Une notification hebdomadaire présentant un résumé des pisteurs, mettant en évidence les changements et mises à jour. + + Principal contrevenant à la vie privée : + + %s a éffectué + tentatives de pistage par heure cette semaine. + tentatives de pistage par heure cette semaine sur mon smartphone. + + Application la plus invasive : + %s a tenté de divulguer des données + fois par heure cette semaine. + fois par heure cette semaine sur mon smartphone. + + Plus grand divulgateur de données : + %s a tenté d\'envoyer des données + fois par jour cette semaine. + fois par jour cette semaine sur mon smartphone. + \ 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 f9ef4a30..8bed1dfd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -188,4 +188,21 @@ Rapporto Settimanale Una notifica settimanale che mostra un riepilogo dei tracker, evidenziando eventuali modifiche e aggiornamenti. + + Principale violatore della privacy: + + %s ha fatto + tentativi di tracciamento all\'ora questa settimana. + tentativi di tracciamento all\'ora questa settimana sul mio smartphone. + + App più invasiva: + %s ha tentato di divulgare dati + volte all\'ora questa settimana. + volte all\'ora questa settimana sul mio smartphone. + + Maggiore divulgatore di dati: + %s ha tentato di inviare dati + volte al giorno questa settimana. + volte al giorno questa settimana sul mio smartphone. + \ No newline at end of file -- GitLab