diff --git a/src/android/org/kde/knotifications/KNotification.java b/src/android/org/kde/knotifications/KNotification.java --- a/src/android/org/kde/knotifications/KNotification.java +++ b/src/android/org/kde/knotifications/KNotification.java @@ -37,6 +37,7 @@ public String channelId; public String channelName; public String channelDescription; + public String group; public int urgency; // see knotification.h diff --git a/src/android/org/kde/knotifications/NotifyByAndroid.java b/src/android/org/kde/knotifications/NotifyByAndroid.java --- a/src/android/org/kde/knotifications/NotifyByAndroid.java +++ b/src/android/org/kde/knotifications/NotifyByAndroid.java @@ -30,6 +30,7 @@ import android.os.Build; import android.text.Html; import android.util.Log; +import java.util.HashMap; import java.util.HashSet; /** Java side of the Android notfication backend. */ @@ -40,14 +41,29 @@ private static final String NOTIFICATION_ACTION = ".org.kde.knotifications.NOTIFICATION_ACTION"; private static final String NOTIFICATION_DELETED = ".org.kde.knotifications.NOTIFICATION_DELETED"; private static final String NOTIFICATION_OPENED = ".org.kde.knotifications.NOTIFICATION_OPENED"; + // the id of the notification triggering an intent private static final String NOTIFICATION_ID_EXTRA = "org.kde.knotifications.NOTIFICATION_ID"; + // the id of the action that was triggered for a notification private static final String NOTIFICATION_ACTION_ID_EXTRA = "org.kde.knotifications.NOTIFICATION_ACTION_ID"; + // the group a notification belongs too + private static final String NOTIFICATION_GROUP_EXTRA = "org.kde.knotifications.NOTIFICATION_GROUP"; + + // notification id offset for group summary notifications + // we need this to stay out of the regular notification's id space (which comes from the C++ side) + // and so we can distinguish if we received actions on regular notifications or group summaries + private static final int NOTIFICATION_GROUP_ID_FLAG = (1 << 24); private android.content.Context m_ctx; private NotificationManager m_notificationManager; private int m_uniquePendingIntentId = 0; private HashSet m_channels = new HashSet(); + private class GroupData { + public HashSet childIds = new HashSet(); + public int groupId; + }; + private HashMap m_groupSummaries = new HashMap(); + public NotifyByAndroid(android.content.Context context) { Log.i(TAG, context.getPackageName()); @@ -116,6 +132,11 @@ builder.setStyle(new Notification.BigTextStyle().bigText(Html.fromHtml(notification.richText))); } + if (notification.group != null) { + createGroupNotification(notification); + builder.setGroup(notification.group); + } + // legacy priority handling for versions without NotificationChannel support if (Build.VERSION.SDK_INT < 26) { switch (notification.urgency) { @@ -156,30 +177,66 @@ // notfication about user closing the notification Intent deleteIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_DELETED); deleteIntent.putExtra(NOTIFICATION_ID_EXTRA, notification.id); + if (notification.group != null) { + deleteIntent.putExtra(NOTIFICATION_GROUP_EXTRA, notification.group); + } Log.i(TAG, deleteIntent.getExtras() + " " + notification.id); builder.setDeleteIntent(PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)); m_notificationManager.notify(notification.id, builder.build()); } - public void close(int id) + public void close(int id, String group) { m_notificationManager.cancel(id); + + if (group != null && m_groupSummaries.containsKey(group)) { + GroupData g = m_groupSummaries.get(group); + g.childIds.remove(id); + if (g.childIds.isEmpty()) { + m_groupSummaries.remove(group); + m_notificationManager.cancel(g.groupId); + } else { + m_groupSummaries.put(group, g); + } + } } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + int id = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1); Log.i(TAG, action + ": " + id + " " + intent.getExtras()); if (action.equals(m_ctx.getPackageName() + NOTIFICATION_ACTION)) { + // user activated one of the custom actions int actionId = intent.getIntExtra(NOTIFICATION_ACTION_ID_EXTRA, -1); notificationActionInvoked(id, actionId); } else if (action.equals(m_ctx.getPackageName() + NOTIFICATION_DELETED)) { - notificationFinished(id); + // user (or system) dismissed the notification - this can happen both for groups and regular notifications + String group = null; + if (intent.hasExtra(NOTIFICATION_GROUP_EXTRA)) { + group = intent.getStringExtra(NOTIFICATION_GROUP_EXTRA); + } + + if ((id & NOTIFICATION_GROUP_ID_FLAG) != 0) { + // entire group has been deleted + m_groupSummaries.remove(group); + } else { + // a single regular notification, so reduce the refcount of the group if there is one + notificationFinished(id); + if (group != null && m_groupSummaries.containsKey(group)) { + // we do not need to handle the case of childIds being empty here, the system will send us a deletion intent for the group too + // this only matters for the logic in close(). + GroupData g = m_groupSummaries.get(group); + g.childIds.remove(id); + m_groupSummaries.put(group, g); + } + } } else if (action.equals(m_ctx.getPackageName() + NOTIFICATION_OPENED)) { + // user tapped the notification Intent newintent = new Intent(m_ctx, m_ctx.getClass()); newintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); m_ctx.startActivity(newintent); @@ -189,4 +246,45 @@ public native void notificationFinished(int notificationId); public native void notificationActionInvoked(int notificationId, int action); + + private void createGroupNotification(KNotification notification) + { + if (m_groupSummaries.containsKey(notification.group)) { + GroupData group = m_groupSummaries.get(notification.group); + group.childIds.add(notification.id); + m_groupSummaries.put(notification.group, group); + return; + } + + GroupData group = new GroupData(); + group.childIds.add(notification.id); + group.groupId = m_uniquePendingIntentId++ + NOTIFICATION_GROUP_ID_FLAG; + m_groupSummaries.put(notification.channelId, group); + + Notification.Builder builder; + if (Build.VERSION.SDK_INT >= 26) { + builder = new Notification.Builder(m_ctx, notification.channelId); + } else { + builder = new Notification.Builder(m_ctx); + } + + if (Build.VERSION.SDK_INT >= 23) { + builder.setSmallIcon((Icon)notification.icon); + } else { + builder.setSmallIcon(m_ctx.getApplicationInfo().icon); + } + builder.setContentTitle(notification.channelName); + builder.setContentText(notification.channelDescription); + builder.setGroup(notification.group); + builder.setGroupSummary(true); + + // monitor for deletion (which happens when the last child notification is closed) + Intent deleteIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_DELETED); + deleteIntent.putExtra(NOTIFICATION_GROUP_EXTRA, notification.group); + deleteIntent.putExtra(NOTIFICATION_ID_EXTRA, group.groupId); + builder.setDeleteIntent(PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)); + + // try to stay out of the normal id space for regular notifications + m_notificationManager.notify(group.groupId, builder.build()); + } } diff --git a/src/notifybyandroid.cpp b/src/notifybyandroid.cpp --- a/src/notifybyandroid.cpp +++ b/src/notifybyandroid.cpp @@ -112,6 +112,10 @@ n.setField("channelName", QAndroidJniObject::fromString(config->readEntry(QLatin1String("Name"))).object()); n.setField("channelDescription", QAndroidJniObject::fromString(config->readEntry(QLatin1String("Comment"))).object()); + if ((notification->flags() & KNotification::SkipGrouping) == 0) { + n.setField("group", QAndroidJniObject::fromString(notification->eventId()).object()); + } + // icon QPixmap pixmap; if (!notification->iconName().isEmpty()) { @@ -155,7 +159,7 @@ void NotifyByAndroid::close(KNotification* notification) { - m_backend.callMethod("close", "(I)V", notification->id()); + m_backend.callMethod("close", "(ILjava/lang/String;)V", notification->id(), QAndroidJniObject::fromString(notification->eventId()).object()); KNotificationPlugin::close(notification); }