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

Commit 4ee8bf9f authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

feat:3056: Display notification on new Weekly report.

parent 2ce2e70f
Loading
Loading
Loading
Loading
+1 −7
Original line number Diff line number Diff line
@@ -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>(NotificationsPresenter::class.java).startListening()

        get<IpScramblingStateUseCase>(IpScramblingStateUseCase::class.java)
        get<TrackersStateUseCase>(TrackersStateUseCase::class.java)
+11 −0
Original line number Diff line number Diff line
@@ -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()
        )
    }
}
+82 −38
Original line number Diff line number Diff line
/*
 * 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)

        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(appContext, NOTIFICATION_IPSCRAMBLING_FLAG)
                hideFlagNotification(NOTIFICATION_IPSCRAMBLING_FLAG)
            }
        }.launchIn(appScope)

        weeklyReportUseCase.currentReport.map { report ->
            if (report != null) {
                showWeeklyReportNotification(report)
            } else {
                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)
    }
}
+4 −3
Original line number Diff line number Diff line
/*
 * 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>(LocalStateRepository::class.java)
    private val backgroundScope by inject<CoroutineScope>(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)
                }
            }
+8 −0
Original line number Diff line number Diff line
@@ -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)
Loading