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 @@ -36,6 +36,7 @@ public String channelId; public String channelName; public String channelDescription; + public String group; public void setIconFromData(byte[] data, int length) { 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 @@ -29,6 +29,7 @@ import android.graphics.drawable.Icon; import android.os.Build; import android.util.Log; +import java.util.HashMap; import java.util.HashSet; /** Java side of the Android notfication backend. */ @@ -39,14 +40,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 int refCount; + public int notificationId; + }; + private HashMap m_groupSummaries = new HashMap(); + public NotifyByAndroid(android.content.Context context) { Log.i(TAG, context.getPackageName()); @@ -94,6 +110,11 @@ // in the single line case this behaves like the regular one, so no special-casing needed builder.setStyle(new Notification.BigTextStyle().bigText(notification.text)); + if (notification.group != null) { + createGroupNotification(notification); + builder.setGroup(notification.group); + } + // taping the notification shows the app Intent intent = new Intent(m_ctx.getPackageName() + NOTIFICATION_OPENED); intent.putExtra(NOTIFICATION_ID_EXTRA, notification.id); @@ -115,30 +136,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.refCount--; + if (g.refCount > 0) { + m_groupSummaries.put(group, g); + } else { + m_groupSummaries.remove(group); + m_notificationManager.cancel(g.notificationId); + } + } } @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 refCount going to 0 here, the system will send us a deletion intent for the group too + // the correct refCount only matters for the logic in close(). + GroupData g = m_groupSummaries.get(group); + g.refCount--; + 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); @@ -148,4 +205,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.refCount++; + m_groupSummaries.put(notification.group, group); + return; + } + + GroupData group = new GroupData(); + group.refCount = 1; + group.notificationId = 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.notificationId); + 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.notificationId, builder.build()); + } } diff --git a/src/notifybyandroid.cpp b/src/notifybyandroid.cpp --- a/src/notifybyandroid.cpp +++ b/src/notifybyandroid.cpp @@ -110,6 +110,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()) { @@ -153,7 +157,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); }