diff --git a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt index f45fdec31a9b83ca11adc4316807102d79e14809..670d7be937be35117651f70a9b8c0673cb88df1f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/AdvancedPrivacyApplication.kt @@ -27,7 +27,6 @@ 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.externalinterfaces.permissions.IPermissionsPrivacyModule import foundation.e.advancedprivacy.externalinterfaces.workers.WeeklyReportWorker import foundation.e.advancedprivacy.trackers.services.UpdateTrackersWorker import foundation.e.lib.telemetry.Telemetry @@ -64,12 +63,7 @@ class AdvancedPrivacyApplication : Application() { get(TrackersStatisticsUseCase::class.java) ) - Notifications.startListening( - this, - get(GetQuickPrivacyStateUseCase::class.java), - get(IPermissionsPrivacyModule::class.java), - get(CoroutineScope::class.java) - ) + get(NotificationsPresenter::class.java).startListening() get(IpScramblingStateUseCase::class.java) get(TrackersStateUseCase::class.java) diff --git a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt index a51d592451c2757ede422a63688aeef563ef8f18..f5fb910ee130af4419ccd6b2bde46ddcfdb3fea8 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/KoinModule.kt @@ -229,4 +229,15 @@ val appModule = module { viewModelOf(::DashboardViewModel) single { WeeklyReportViewFactory() } + + single { + NotificationsPresenter( + context = androidContext(), + getQuickPrivacyStateUseCase = get(), + permissionsPrivacyModule = get(), + weeklyReportUseCase = get(), + weeklyReportViewFactory = get(), + appScope = get() + ) + } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt b/app/src/main/java/foundation/e/advancedprivacy/NotificationsPresenter.kt similarity index 62% rename from app/src/main/java/foundation/e/advancedprivacy/Notifications.kt rename to app/src/main/java/foundation/e/advancedprivacy/NotificationsPresenter.kt index 4051ce2bf9f479a392211dbe16d3eb78f8601774..9246f5c16f871bda05cf4e59eef1d293102d5d96 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/Notifications.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/NotificationsPresenter.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 E FOUNDATION + * Copyright (C) 2024 - 2025 E FOUNDATION * Copyright (C) 2022 - 2023 MURENA SAS * * This program is free software: you can redistribute it and/or modify @@ -24,29 +24,47 @@ import android.content.Context import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import foundation.e.advancedprivacy.core.utils.notificationBuilder import foundation.e.advancedprivacy.domain.entities.CHANNEL_FAKE_LOCATION_FLAG import foundation.e.advancedprivacy.domain.entities.CHANNEL_FIRST_BOOT import foundation.e.advancedprivacy.domain.entities.CHANNEL_IPSCRAMBLING_FLAG import foundation.e.advancedprivacy.domain.entities.CHANNEL_TRACKER_FLAG +import foundation.e.advancedprivacy.domain.entities.CHANNEL_WEEKLYREPORT import foundation.e.advancedprivacy.domain.entities.FeatureMode import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_FAKE_LOCATION_FLAG import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_FIRST_BOOT import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_IPSCRAMBLING_FLAG +import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_WEEKLYREPORT import foundation.e.advancedprivacy.domain.entities.NotificationContent +import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase +import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule +import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory import foundation.e.advancedprivacy.main.MainActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import timber.log.Timber -object Notifications { - fun showFirstBootNotification(context: Context) { - createNotificationFirstBootChannel(context) +// @SuppressLint("MissingPermission") +class NotificationsPresenter( + private val context: Context, + private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, + private val permissionsPrivacyModule: IPermissionsPrivacyModule, + private val weeklyReportUseCase: WeeklyReportUseCase, + private val weeklyReportViewFactory: WeeklyReportViewFactory, + private val appScope: CoroutineScope +) { + + private val notificationManager = NotificationManagerCompat.from(context) + + fun showFirstBootNotification() { + createNotificationFirstBootChannel() val notificationBuilder: NotificationCompat.Builder = notificationBuilder( context, NotificationContent( @@ -61,49 +79,44 @@ object Notifications { ) .setAutoCancel(true) - NotificationManagerCompat.from(context).notify( - NOTIFICATION_FIRST_BOOT, - notificationBuilder.build() - ) + try { + NotificationManagerCompat.from(context).notify( + NOTIFICATION_FIRST_BOOT, + notificationBuilder.build() + ) + } catch (e: SecurityException) { + Timber.e(e, "Unexpected SecurityException while posting notification, we should have rights.") + } } - fun startListening( - appContext: Context, - getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - permissionsPrivacyModule: IPermissionsPrivacyModule, - appScope: CoroutineScope - ) { + fun startListening() { createNotificationFlagChannel( - context = appContext, - permissionsPrivacyModule = permissionsPrivacyModule, channelId = CHANNEL_FAKE_LOCATION_FLAG, channelName = R.string.notifications_fake_location_channel_name, channelDescription = R.string.notifications_fake_location_channel_description ) createNotificationFlagChannel( - context = appContext, - permissionsPrivacyModule = permissionsPrivacyModule, channelId = CHANNEL_IPSCRAMBLING_FLAG, channelName = R.string.notifications_ipscrambling_channel_name, channelDescription = R.string.notifications_ipscrambling_channel_description ) createNotificationFlagChannel( - context = appContext, - permissionsPrivacyModule = permissionsPrivacyModule, channelId = CHANNEL_TRACKER_FLAG, channelName = R.string.notifications_tracker_channel_name, channelDescription = R.string.notifications_tracker_channel_description ) + createWeeklyReportChannel() + getQuickPrivacyStateUseCase.locationMode.map { it != FeatureMode.VULNERABLE }.distinctUntilChanged().onEach { if (it) { - showFlagNotification(appContext, NOTIFICATION_FAKE_LOCATION_FLAG) + showFlagNotification(NOTIFICATION_FAKE_LOCATION_FLAG) } else { - hideFlagNotification(appContext, NOTIFICATION_FAKE_LOCATION_FLAG) + hideFlagNotification(NOTIFICATION_FAKE_LOCATION_FLAG) } }.launchIn(appScope) @@ -111,14 +124,22 @@ object Notifications { it != FeatureState.OFF }.distinctUntilChanged().onEach { if (it) { - showFlagNotification(appContext, NOTIFICATION_IPSCRAMBLING_FLAG) + showFlagNotification(NOTIFICATION_IPSCRAMBLING_FLAG) + } else { + hideFlagNotification(NOTIFICATION_IPSCRAMBLING_FLAG) + } + }.launchIn(appScope) + + weeklyReportUseCase.currentReport.map { report -> + if (report != null) { + showWeeklyReportNotification(report) } else { - hideFlagNotification(appContext, NOTIFICATION_IPSCRAMBLING_FLAG) + notificationManager.cancel(NOTIFICATION_WEEKLYREPORT) } }.launchIn(appScope) } - private fun createNotificationFirstBootChannel(context: Context) { + private fun createNotificationFirstBootChannel() { val channel = NotificationChannel( CHANNEL_FIRST_BOOT, context.getString(R.string.notifications_first_boot_channel_name), @@ -127,13 +148,7 @@ object Notifications { NotificationManagerCompat.from(context).createNotificationChannel(channel) } - private fun createNotificationFlagChannel( - context: Context, - permissionsPrivacyModule: IPermissionsPrivacyModule, - channelId: String, - @StringRes channelName: Int, - @StringRes channelDescription: Int - ) { + private fun createNotificationFlagChannel(channelId: String, @StringRes channelName: Int, @StringRes channelDescription: Int) { val channel = NotificationChannel( channelId, context.getString(channelName), @@ -144,10 +159,21 @@ object Notifications { NotificationManagerCompat.from(context).createNotificationChannel(channel) } - private fun showFlagNotification(context: Context, id: Int) { + private fun createWeeklyReportChannel() { + val channel = NotificationChannel( + CHANNEL_WEEKLYREPORT, + context.getString(R.string.notifications_weeklyreport_channel_name), + NotificationManager.IMPORTANCE_LOW + ) + channel.description = context.getString(R.string.notifications_weeklyreport_channel_description) + + permissionsPrivacyModule.setBlockable(channel) + notificationManager.createNotificationChannel(channel) + } + + private fun showFlagNotification(id: Int) { when (id) { NOTIFICATION_FAKE_LOCATION_FLAG -> showFlagNotification( - context = context, id = NOTIFICATION_FAKE_LOCATION_FLAG, content = NotificationContent( channelId = CHANNEL_FAKE_LOCATION_FLAG, @@ -160,7 +186,6 @@ object Notifications { ) ) NOTIFICATION_IPSCRAMBLING_FLAG -> showFlagNotification( - context = context, id = NOTIFICATION_IPSCRAMBLING_FLAG, content = NotificationContent( channelId = CHANNEL_IPSCRAMBLING_FLAG, @@ -176,15 +201,34 @@ object Notifications { } } - private fun showFlagNotification(context: Context, id: Int, content: NotificationContent) { + private fun showFlagNotification(id: Int, content: NotificationContent) { val builder = notificationBuilder(context, content) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) + notificationManager.notify(id, builder.build()) + } + + fun showWeeklyReportNotification(report: DisplayableReport) { + val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_WEEKLYREPORT) + weeklyReportViewFactory.populateNotification(report, notificationBuilder) + notificationBuilder.setColor(ContextCompat.getColor(context, R.color.launcher_icon_background)) + .setColorized(true) + .setSmallIcon(R.drawable.ic_advanced_privacy_for_notification) + + notificationBuilder.setSmallIcon(R.drawable.ic_shield_alert) + notificationBuilder.setAutoCancel(true) - NotificationManagerCompat.from(context).notify(id, builder.build()) + val goToTrackersIntent = MainActivity.deepLinkBuilder(context) + .setDestination(R.id.trackersFragment) + notificationBuilder.setContentIntent(goToTrackersIntent.createPendingIntent()) + + notificationManager.notify( + NOTIFICATION_WEEKLYREPORT, + notificationBuilder.build() + ) } - private fun hideFlagNotification(context: Context, id: Int) { + private fun hideFlagNotification(id: Int) { NotificationManagerCompat.from(context).cancel(id) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt index 8ddb1b380bbf74223fcbb52c15980aaefbb0fff2..22549bdb762075b0e6f8cbd84867973f06838125 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt @@ -1,6 +1,6 @@ /* * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 - 2024 E FOUNDATION + * Copyright (C) 2022 - 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 @@ -21,7 +21,7 @@ package foundation.e.advancedprivacy.common import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import foundation.e.advancedprivacy.Notifications +import foundation.e.advancedprivacy.NotificationsPresenter import foundation.e.advancedprivacy.core.utils.goAsync import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository import kotlinx.coroutines.CoroutineScope @@ -31,12 +31,13 @@ class BootCompletedReceiver : BroadcastReceiver() { private val localStateRepository by inject(LocalStateRepository::class.java) private val backgroundScope by inject(CoroutineScope::class.java) + private val notificationsPresenter: NotificationsPresenter by inject(NotificationsPresenter::class.java) override fun onReceive(context: Context, intent: Intent?) { if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { goAsync(backgroundScope) { if (localStateRepository.isFirstBoot()) { - Notifications.showFirstBootNotification(context) + notificationsPresenter.showFirstBootNotification() localStateRepository.setFirstBoot(false) } } diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt index 24100a2823d156d3985cfd64773d496aa39f2416..5885f94cb35d0515d2269fb98a9eff54083cb7e6 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt @@ -28,6 +28,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import foundation.e.advancedprivacy.NotificationsPresenter import foundation.e.advancedprivacy.R import foundation.e.advancedprivacy.common.BindingListAdapter import foundation.e.advancedprivacy.common.BindingViewHolder @@ -51,6 +52,7 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment private val weeklyReportUseCase: WeeklyReportUseCase by inject() private val appListRepository: AppListRepository by inject() private val trackersRepository: TrackersRepository by inject() + private val notificationsPresenter: NotificationsPresenter by inject() private val reportsFactory: WeeklyReportViewFactory by inject() private lateinit var binding: DebugWeeklyReportFragmentBinding @@ -94,6 +96,12 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment }" } + holder.binding.showNotification.setOnClickListener { + report.first?.let { + notificationsPresenter.showWeeklyReportNotification(it) + } + } + holder.binding.share.setOnClickListener { val bmp = report.first?.let { reportsFactory.createShareBmp(requireContext(), it) 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 af2eb301cde7ee4203e4a61c87162af56d01e40f..b24b38ddbc54672f5ba99b3f3c3e6a20e144fd25 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 @@ -27,6 +27,8 @@ import android.view.LayoutInflater import android.view.View import android.view.View.MeasureSpec import android.view.ViewGroup +import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.toBitmap import foundation.e.advancedprivacy.databinding.WeeklyReportItemTextBinding import foundation.e.advancedprivacy.databinding.WeeklyReportShareTemplateBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport @@ -82,6 +84,12 @@ class WeeklyReportViewFactory() { return bmp } + fun populateNotification(report: DisplayableReport, builder: NotificationCompat.Builder) { + builder.setContentTitle(getTitle(report)) + builder.setContentText(getDescription(report)) + getIcon(report)?.toBitmap()?.let { builder.setLargeIcon(it) } + } + private fun getTitle(report: DisplayableReport): CharSequence { return report.report.labelId.name } diff --git a/app/src/main/res/drawable/ic_advanced_privacy_for_notification.xml b/app/src/main/res/drawable/ic_advanced_privacy_for_notification.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fc049a2c85203216d5e9816bd3c972be46ece91 --- /dev/null +++ b/app/src/main/res/drawable/ic_advanced_privacy_for_notification.xml @@ -0,0 +1,35 @@ + + + + + + + diff --git a/app/src/main/res/layout/debug_weekly_report_item.xml b/app/src/main/res/layout/debug_weekly_report_item.xml index 128f86798fa774e3c385c6c69ca17919c2f8f2a9..6c2478545376fc4a6c682abaa3bf3913a4ff48f1 100644 --- a/app/src/main/res/layout/debug_weekly_report_item.xml +++ b/app/src/main/res/layout/debug_weekly_report_item.xml @@ -50,8 +50,6 @@ android:layout_height="48dp" android:text="@string/debug_weekly_report_item_notification" android:layout_margin="4dp" - - android:enabled="false" /> Sehen Sie, wie /e/OS mein Gerät vor Tracking-Versuchen schützt! \nWarum mobile App-Tracker eine der größten Bedrohungen für Ihre Privatsphäre und Freiheit sind: Lesen Sie unseren Datenschutzleitfaden unter Wöchentlicher Bericht: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_DE.pdf + 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 a6eb3b552c05945ce3cb47578d2ace2a892b756d..ed9ebdcbc764df78caaf6364f150f6fb5fdd8502 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -159,4 +159,6 @@ ¡Vea cómo /e/OS protege mi dispositivo de intentos de seguimiento! \nPor qué los rastreadores de aplicaciones móviles son una de las amenazas más impactantes para su privacidad y su libertad: lea nuestra Guía de Privacidad en Informe semanal: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_ES.pdf + 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 b6e96c865c4c3bf419513e667b29a367314d953d..adedad3bf180c43b9db3c5baf1c3c402b352c425 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -151,11 +151,12 @@ Mur de la honte %s fuites détectées - Rapport Hebdomadaire Partagez ce rapport et montrez aux autres ce qui se passe ! Partager Voyez comment /e/OS protège mon appareil des tentatives de traçage ! \nPourquoi les trackers d\'applications mobiles sont l\'une des menaces les plus impactantes pour votre vie privée et votre liberté : lisez notre Guide de Confidentialité : Rapport Hebdomadaire : https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_FR.pdf + 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 00239f0f643dc01c8aab4d89b03349e3ae8dd8ef..be7cc7a66561cf240b4382b7d45c88039c4acd93 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -160,4 +160,6 @@ Rapporto settimanale: https://e.foundation/wp-content/uploads/2025/01/White_Paper_-_Privacy_-_IT.pdf + 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 5840648f491df22d67f58a5ae8ff6e18acf04c71..921f63cddca541ba96dc24e1d20aa0edce2f7aab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@