From 8eaffc6e948273b5bc806ff573d60ae12918653d Mon Sep 17 00:00:00 2001
From: Matthieu <61561059+Wv5twkFEKh54vo4tta9yu7dHa3@users.noreply.github.com>
Date: Mon, 22 Jan 2024 19:21:34 +0100
Subject: [PATCH 1/2] Split calendar notifications into channels

---
 .../android/calendar/AllInOneActivity.java    |  6 +-
 .../calendar/alerts/AlertReceiver.java        | 14 ++--
 .../android/calendar/alerts/AlertService.java | 32 ++++----
 .../java/com/android/calendar/alerts/Utils.kt | 78 +++++++++++++++++++
 app/src/main/res/values/strings.xml           |  3 +
 5 files changed, 107 insertions(+), 26 deletions(-)
 create mode 100644 app/src/main/java/com/android/calendar/alerts/Utils.kt

diff --git a/app/src/main/java/com/android/calendar/AllInOneActivity.java b/app/src/main/java/com/android/calendar/AllInOneActivity.java
index 356a28803d..0b003f9563 100644
--- a/app/src/main/java/com/android/calendar/AllInOneActivity.java
+++ b/app/src/main/java/com/android/calendar/AllInOneActivity.java
@@ -267,12 +267,12 @@ protected void onCreate(Bundle icicle) {
         // This needs to be created before setContentView
         mController = CalendarController.getInstance(this);
 
-        // Create notification channel
-        AlertService.createChannels(this);
-
         // Check and ask for most needed permissions
         checkAppPermissions();
 
+        // Create notification channels
+        AlertService.createChannels(this);
+
         // Get time from intent or icicle
         long timeMillis = -1;
         int viewType = -1;
diff --git a/app/src/main/java/com/android/calendar/alerts/AlertReceiver.java b/app/src/main/java/com/android/calendar/alerts/AlertReceiver.java
index a03258a770..4ed6e47674 100644
--- a/app/src/main/java/com/android/calendar/alerts/AlertReceiver.java
+++ b/app/src/main/java/com/android/calendar/alerts/AlertReceiver.java
@@ -17,8 +17,6 @@
 
 package com.android.calendar.alerts;
 
-import static com.android.calendar.alerts.AlertService.ALERT_CHANNEL_ID;
-
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -230,10 +228,10 @@ private static PendingIntent createAlertActivityIntent(Context context) {
     }
 
     public static NotificationWrapper makeBasicNotification(Context context, String title,
-            String summaryText, long startMillis, long endMillis, long eventId,
+            String summaryText, long startMillis, long endMillis, long eventId, long calendarId,
             int notificationId, boolean doPopup, int priority) {
         Notification n = buildBasicNotification(new Notification.Builder(context),
-                context, title, summaryText, startMillis, endMillis, eventId, notificationId,
+                context, title, summaryText, startMillis, endMillis, eventId, calendarId, notificationId,
                 doPopup, priority, false);
         return new NotificationWrapper(n, notificationId, eventId, startMillis, endMillis, doPopup);
     }
@@ -247,7 +245,7 @@ public static boolean isResolveIntent(Context context, Intent intent) {
 
     private static Notification buildBasicNotification(Notification.Builder notificationBuilder,
             Context context, String title, String summaryText, long startMillis, long endMillis,
-            long eventId, int notificationId, boolean doPopup, int priority,
+            long eventId, long calendarId, int notificationId, boolean doPopup, int priority,
             boolean addActionButtons) {
         Resources resources = context.getResources();
         if (title == null || title.length() == 0) {
@@ -274,7 +272,7 @@ private static Notification buildBasicNotification(Notification.Builder notifica
 
         // Add setting channel ID for Oreo or later
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            notificationBuilder.setChannelId(ALERT_CHANNEL_ID);
+            notificationBuilder.setChannelId(UtilsKt.channelId(calendarId));
         }
 
         if (doPopup) {
@@ -346,10 +344,10 @@ private static Notification buildBasicNotification(Notification.Builder notifica
      */
     public static NotificationWrapper makeExpandingNotification(Context context, String title,
             String summaryText, String description, long startMillis, long endMillis, long eventId,
-            int notificationId, boolean doPopup, int priority) {
+            long calendarId, int notificationId, boolean doPopup, int priority) {
         Notification.Builder basicBuilder = new Notification.Builder(context);
         Notification notification = buildBasicNotification(basicBuilder, context, title,
-                summaryText, startMillis, endMillis, eventId, notificationId, doPopup,
+                summaryText, startMillis, endMillis, eventId, calendarId, notificationId, doPopup,
                 priority, true);
 
         // Create a new-style expanded notification
diff --git a/app/src/main/java/com/android/calendar/alerts/AlertService.java b/app/src/main/java/com/android/calendar/alerts/AlertService.java
index 410d306b6b..7933429a7c 100644
--- a/app/src/main/java/com/android/calendar/alerts/AlertService.java
+++ b/app/src/main/java/com/android/calendar/alerts/AlertService.java
@@ -70,7 +70,7 @@
  */
 public class AlertService extends Service {
 
-    public static final String ALERT_CHANNEL_ID = "alert_channel_01";
+    public static final String ALERT_CHANNEL_GROUP_ID = "alert_channel_group_01";
     public static final String FOREGROUND_CHANNEL_ID = "foreground_channel_01";
 
     // Hard limit to the number of notifications displayed.
@@ -89,6 +89,7 @@ public class AlertService extends Service {
         CalendarAlerts.BEGIN,                   // 9
         CalendarAlerts.END,                     // 10
         CalendarAlerts.DESCRIPTION,             // 11
+        CalendarAlerts.CALENDAR_ID,             // 12
     };
     private static final String TAG = "AlertService";
     private static final int ALERT_INDEX_ID = 0;
@@ -103,6 +104,7 @@ public class AlertService extends Service {
     private static final int ALERT_INDEX_BEGIN = 9;
     private static final int ALERT_INDEX_END = 10;
     private static final int ALERT_INDEX_DESCRIPTION = 11;
+    private static final int ALERT_INDEX_CALENDAR_ID = 12;
     private static final String ACTIVE_ALERTS_SELECTION = "(" + CalendarAlerts.STATE + "=? OR "
             + CalendarAlerts.STATE + "=?) AND " + CalendarAlerts.ALARM_TIME + "<=";
     private static final String[] ACTIVE_ALERTS_SELECTION_ARGS = new String[] {
@@ -282,7 +284,7 @@ public static boolean generateAlerts(Context context, NotificationMgr nm,
                 String summaryText = AlertUtils.formatTimeLocation(context, info.startMillis,
                         info.allDay, info.location);
                 notification = AlertReceiver.makeBasicNotification(context, info.eventName,
-                        summaryText, info.startMillis, info.endMillis, info.eventId,
+                        summaryText, info.startMillis, info.endMillis, info.eventId, info.calendarId,
                         AlertUtils.EXPIRED_GROUP_NOTIFICATION_ID, false,
                         Notification.PRIORITY_MIN);
             } else {
@@ -473,6 +475,7 @@ static int processQuery(final Cursor alertCursor, final Context context,
             while (alertCursor.moveToNext()) {
                 final long alertId = alertCursor.getLong(ALERT_INDEX_ID);
                 final long eventId = alertCursor.getLong(ALERT_INDEX_EVENT_ID);
+                final long calendarId = alertCursor.getLong(ALERT_INDEX_CALENDAR_ID);
                 final int minutes = alertCursor.getInt(ALERT_INDEX_MINUTES);
                 final String eventName = alertCursor.getString(ALERT_INDEX_TITLE);
                 final String description = alertCursor.getString(ALERT_INDEX_DESCRIPTION);
@@ -511,6 +514,7 @@ static int processQuery(final Cursor alertCursor, final Context context,
                     msgBuilder.append("alertCursor result: alarmTime:").append(alarmTime)
                             .append(" alertId:").append(alertId)
                             .append(" eventId:").append(eventId)
+                            .append(" calendarId:").append(calendarId)
                             .append(" state: ").append(state)
                             .append(" minutes:").append(minutes)
                             .append(" declined:").append(declined)
@@ -590,7 +594,7 @@ static int processQuery(final Cursor alertCursor, final Context context,
 
                 // TODO: Prefer accepted events in case of ties.
                 NotificationInfo newInfo = new NotificationInfo(eventName, location,
-                        description, beginTime, endTime, eventId, allDay, newAlert);
+                        description, beginTime, endTime, eventId, calendarId, allDay, newAlert);
 
                 // Adjust for all day events to ensure the right bucket.  Don't use the 1/4 event
                 // duration grace period for these.
@@ -707,8 +711,8 @@ private static void postNotification(NotificationInfo info, String summaryText,
 
         String tickerText = getTickerText(info.eventName, info.location);
         NotificationWrapper notification = AlertReceiver.makeExpandingNotification(context,
-                info.eventName, summaryText, info.description, info.startMillis,
-                info.endMillis, info.eventId, notificationId, prefs.getDoPopup(), priorityVal);
+                info.eventName, summaryText, info.description, info.startMillis, info.endMillis,
+                info.eventId, info.calendarId, notificationId, prefs.getDoPopup(), priorityVal);
 
         boolean quietUpdate = true;
         String ringtone = NotificationPrefs.EMPTY_RINGTONE;
@@ -967,16 +971,13 @@ public IBinder onBind(Intent intent) {
 
     public static void createChannels(Context context) {
         if (Utils.isOreoOrLater()) {
-            // Create notification channel
-            NotificationMgr nm = new NotificationMgrWrapper(
-                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
+            NotificationManager nm =
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
 
-            NotificationChannel channel  = new NotificationChannel(
-                    ALERT_CHANNEL_ID,
-                    context.getString(R.string.standalone_app_label),
-                    NotificationManager.IMPORTANCE_HIGH);
-            channel.enableLights(true);
+            // Create a channel per calendar (so that the user can turn it off with granularity)
+            UtilsKt.createPerCalendarChannels(context, nm);
 
+            // Create a "Background tasks" channel to keep the app alive
             NotificationChannel foregroundChannel = new NotificationChannel(
                     FOREGROUND_CHANNEL_ID,
                     context.getString(R.string.foreground_notification_channel_name),
@@ -984,7 +985,6 @@ public static void createChannels(Context context) {
             foregroundChannel.setDescription(
                     context.getString(R.string.foreground_notification_channel_description));
 
-            nm.createNotificationChannel(channel);
             nm.createNotificationChannel(foregroundChannel);
         }
     }
@@ -1056,17 +1056,19 @@ static class NotificationInfo {
         long startMillis;
         long endMillis;
         long eventId;
+        long calendarId;
         boolean allDay;
         boolean newAlert;
 
         NotificationInfo(String eventName, String location, String description, long startMillis,
-                         long endMillis, long eventId, boolean allDay, boolean newAlert) {
+                         long endMillis, long eventId, long calendarId, boolean allDay, boolean newAlert) {
             this.eventName = eventName;
             this.location = location;
             this.description = description;
             this.startMillis = startMillis;
             this.endMillis = endMillis;
             this.eventId = eventId;
+            this.calendarId = calendarId;
             this.newAlert = newAlert;
             this.allDay = allDay;
         }
diff --git a/app/src/main/java/com/android/calendar/alerts/Utils.kt b/app/src/main/java/com/android/calendar/alerts/Utils.kt
new file mode 100644
index 0000000000..30ba68d187
--- /dev/null
+++ b/app/src/main/java/com/android/calendar/alerts/Utils.kt
@@ -0,0 +1,78 @@
+package com.android.calendar.alerts
+
+import android.app.NotificationChannel
+import android.app.NotificationChannelGroup
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import android.provider.CalendarContract
+import androidx.annotation.RequiresApi
+import androidx.core.database.getStringOrNull
+import com.android.calendar.Utils
+import com.android.calendar.alerts.AlertService.ALERT_CHANNEL_GROUP_ID
+import ws.xsoh.etar.R
+
+
+val PROJECTION = arrayOf(
+    CalendarContract.Calendars._ID,
+    CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
+)
+
+data class CalendarChannel(val id: Long, val displayName: String?)
+
+fun channelId(id: Long) = "calendar$id"
+
+@RequiresApi(Build.VERSION_CODES.O)
+fun createPerCalendarChannels(context: Context, nm: NotificationManager) {
+    val calendars: MutableList<CalendarChannel> = mutableListOf()
+
+    // Make sure we have the right permissions to access the calendar list
+    if (!Utils.isCalendarPermissionGranted(context, false)) return
+
+    context.contentResolver.query(
+        CalendarContract.Calendars.CONTENT_URI,
+        PROJECTION,
+        null,
+        null,
+        CalendarContract.Calendars.ACCOUNT_NAME
+    )?.use {
+        while (it.moveToNext()) {
+            val id = it.getLong(PROJECTION.indexOf(CalendarContract.Calendars._ID))
+            val displayName =
+                it.getStringOrNull(PROJECTION.indexOf(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME))
+
+            calendars.add(CalendarChannel(id, displayName))
+        }
+    }
+
+    // Make NotificationChannel group for calendars
+    nm.createNotificationChannelGroup(
+        NotificationChannelGroup(
+            ALERT_CHANNEL_GROUP_ID, context.getString(R.string.calendars)
+        )
+    )
+
+    // Fetch list of existing notification channels
+    val toDelete = nm.notificationChannels.filter { channel: NotificationChannel ->
+        // Only consider the channels of the calendar group
+        channel.group == ALERT_CHANNEL_GROUP_ID
+            // And only keep those that don't correspond to calendars (so those we want to delete)
+            && !calendars.any { channelId(it.id) == channel.id }
+    }
+
+    // We want to delete these channels because they don't correspond to any calendars (anymore)
+    toDelete.forEach { nm.deleteNotificationChannel(it.id) }
+
+    val channels = calendars.map {
+        NotificationChannel(
+            channelId(it.id),
+            if (it.displayName.isNullOrBlank()) context.getString(R.string.preferences_calendar_no_display_name) else it.displayName,
+            NotificationManager.IMPORTANCE_HIGH
+        ).apply {
+            enableLights(true)
+            group = ALERT_CHANNEL_GROUP_ID
+        }
+    }
+
+    channels.forEach { nm.createNotificationChannel(it) }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0b0baf56de..d5c57654b5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -805,4 +805,7 @@
 
     <string name="pref_theme_default" translatable="false">light</string>
 
+    <!-- Name of notification channel group -->
+    <string name="calendars">Calendars</string>
+
 </resources>

From 6cfc6fd4d1c3c37d6ae5ab4acb2322c60e0af2fe Mon Sep 17 00:00:00 2001
From: Matthieu <61561059+Wv5twkFEKh54vo4tta9yu7dHa3@users.noreply.github.com>
Date: Thu, 25 Jan 2024 16:05:57 +0100
Subject: [PATCH 2/2] Add link to manage notifications to settings

---
 .../calendar/settings/CalendarPreferences.kt     | 16 +++++++++++++++-
 app/src/main/res/values/strings.xml              |  2 ++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt b/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt
index 2a17ce9b5f..4743d602d1 100644
--- a/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt
+++ b/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt
@@ -25,6 +25,7 @@ import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
 import android.os.Bundle
 import android.provider.CalendarContract
+import android.provider.Settings
 import android.util.TypedValue
 import androidx.appcompat.app.AlertDialog
 import androidx.core.content.ContextCompat
@@ -34,6 +35,7 @@ import androidx.preference.PreferenceCategory
 import androidx.preference.PreferenceFragmentCompat
 import androidx.preference.SwitchPreference
 import com.android.calendar.Utils
+import com.android.calendar.alerts.channelId
 import com.android.calendar.persistence.CalendarRepository
 import ws.xsoh.etar.R
 
@@ -133,11 +135,23 @@ class CalendarPreferences : PreferenceFragmentCompat() {
             isSelectable = false
         }
 
-
         if (!isLocalAccount) {
             screen.addPreference(synchronizePreference)
         }
+
         screen.addPreference(visiblePreference)
+
+        if(Utils.isOreoOrLater()){
+            val notificationPreference = Preference(context).apply {
+                title = getString(R.string.preferences_manage_notifications)
+                intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
+                    putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
+                    putExtra(Settings.EXTRA_CHANNEL_ID, channelId(this@CalendarPreferences.calendarId))
+                }
+            }
+            screen.addPreference(notificationPreference)
+        }
+
         screen.addPreference(colorPreference)
         if (isLocalAccount) {
             screen.addPreference(displayNamePreference)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d5c57654b5..34d50e494c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -779,6 +779,8 @@
     <string name="preferences_calendar_info_category">Calendar information</string>
     <string name="preferences_calendar_color">Color</string>
     <string name="preferences_calendar_visible">Display events</string>
+    <!-- Button to access system notification preference for a given calendar -->
+    <string name="preferences_manage_notifications">Manage notifications</string>
     <string name="preferences_calendar_synchronize">Synchronize this calendar</string>
     <string name="preferences_calendar_color_warning_title">Warning</string>
     <string name="preferences_calendar_color_warning_message">Changing the color may be reverted when the calendar is synchronized again.\n\nIn DAVx⁵ \'Manage calendar colors\' can be disabled to prevent this.</string>