From 265c10587a0716f46cd17441dea18da303e0725e Mon Sep 17 00:00:00 2001 From: Christoph Studer Date: Wed, 23 Jul 2014 17:14:33 +0200 Subject: [PATCH] NoMan: Optimize grouped notifications When SysUI is the only notification listener, drop group children if there is a group summary in NoMan. There are two changes needed to achieve this: 1. Cancel children when a summary is posted 2. Drop children if there is a summary Bug: 18460939 Change-Id: I5a3f30b0b23b2a784783749f58352c05a7fb9e59 --- .../com/android/server/EventLogTags.logtags | 4 +- .../NotificationManagerService.java | 188 ++++++++++++++++-- 2 files changed, 170 insertions(+), 22 deletions(-) diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags index 8417cccdb6492..f04487e62cf59 100644 --- a/services/core/java/com/android/server/EventLogTags.logtags +++ b/services/core/java/com/android/server/EventLogTags.logtags @@ -51,8 +51,8 @@ option java_package com.android.server # --------------------------- # NotificationManagerService.java # --------------------------- -# when a NotificationManager.notify is called -2750 notification_enqueue (uid|1|5),(pid|1|5),(pkg|3),(id|1|5),(tag|3),(userid|1|5),(notification|3),(update|1) +# when a NotificationManager.notify is called. status: 0=post, 1=update, 2=ignored +2750 notification_enqueue (uid|1|5),(pid|1|5),(pkg|3),(id|1|5),(tag|3),(userid|1|5),(notification|3),(status|1) # when someone tries to cancel a notification, the notification manager sometimes # calls this with flags too 2751 notification_cancel (uid|1|5),(pid|1|5),(pkg|3),(id|1|5),(tag|3),(userid|1|5),(required_flags|1),(forbidden_flags|1),(reason|1|5),(listener|3) diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6958ff842b8af..6b88aae7486f0 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -116,6 +116,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; @@ -139,6 +140,7 @@ public class NotificationManagerService extends SystemService { static final int SHORT_DELAY = 2000; // 2 seconds static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; + static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION; @@ -166,6 +168,15 @@ public class NotificationManagerService extends SystemService { static final float MATCHES_CALL_FILTER_TIMEOUT_AFFINITY = ValidateNotificationPeople.STARRED_CONTACT; + /** notification_enqueue status value for a newly enqueued notification. */ + private static final int EVENTLOG_ENQUEUE_STATUS_NEW = 0; + + /** notification_enqueue status value for an existing notification. */ + private static final int EVENTLOG_ENQUEUE_STATUS_UPDATE = 1; + + /** notification_enqueue status value for an ignored notification. */ + private static final int EVENTLOG_ENQUEUE_STATUS_IGNORED = 2; + private IActivityManager mAm; AudioManager mAudioManager; StatusBarManagerInternal mStatusBar; @@ -209,6 +220,7 @@ public class NotificationManagerService extends SystemService { final ArrayMap mNotificationsByKey = new ArrayMap(); final ArrayList mToastQueue = new ArrayList(); + final ArrayMap mSummaryByGroupKey = new ArrayMap<>(); ArrayList mLights = new ArrayList(); NotificationRecord mLedNotification; @@ -251,6 +263,7 @@ public class NotificationManagerService extends SystemService { private static final int REASON_LISTENER_CANCEL = 10; private static final int REASON_LISTENER_CANCEL_ALL = 11; private static final int REASON_GROUP_SUMMARY_CANCELED = 12; + private static final int REASON_GROUP_OPTIMIZATION = 13; private static class Archive { final int mBufferSize; @@ -1658,6 +1671,16 @@ public class NotificationManagerService extends SystemService { pw.println("\n Condition providers:"); mConditionProviders.dump(pw, filter); + + pw.println("\n Group summaries:"); + for (Entry entry : mSummaryByGroupKey.entrySet()) { + NotificationRecord r = entry.getValue(); + pw.println(" " + entry.getKey() + " -> " + r.getKey()); + if (mNotificationsByKey.get(r.getKey()) != r) { + pw.println("!!!!!!LEAK: Record not found in mNotificationsByKey."); + r.dump(pw, " ", getContext()); + } + } } } @@ -1779,16 +1802,34 @@ public class NotificationManagerService extends SystemService { // Retain ranking information from previous record r.copyRankingInformation(old); } - mRankingHelper.extractSignals(r); + + // Handle grouped notifications and bail out early if we + // can to avoid extracting signals. + handleGroupedNotificationLocked(r, old, callingUid, callingPid); + boolean ignoreNotification = + removeUnusedGroupedNotificationLocked(r, callingUid, callingPid); // This conditional is a dirty hack to limit the logging done on // behalf of the download manager without affecting other apps. if (!pkg.equals("com.android.providers.downloads") || Log.isLoggable("DownloadManager", Log.VERBOSE)) { + int enqueueStatus = EVENTLOG_ENQUEUE_STATUS_NEW; + if (ignoreNotification) { + enqueueStatus = EVENTLOG_ENQUEUE_STATUS_IGNORED; + } else if (old != null) { + enqueueStatus = EVENTLOG_ENQUEUE_STATUS_UPDATE; + } EventLogTags.writeNotificationEnqueue(callingUid, callingPid, pkg, id, tag, userId, notification.toString(), - (old != null) ? 1 : 0); + enqueueStatus); } + + if (ignoreNotification) { + return; + } + + mRankingHelper.extractSignals(r); + // 3. Apply local rules // blocked apps @@ -1805,16 +1846,6 @@ public class NotificationManagerService extends SystemService { return; } - // Clear out group children of the old notification if the update causes the - // group summary to go away. This happens when the old notification was a - // summary and the new one isn't, or when the old notification was a summary - // and its group key changed. - if (old != null && old.getNotification().isGroupSummary() && - (!notification.isGroupSummary() || - !old.getGroupKey().equals(r.getGroupKey()))) { - cancelGroupChildrenLocked(old, callingUid, callingPid, null); - } - int index = indexOfNotificationLocked(n.getKey()); if (index < 0) { mNotificationList.add(r); @@ -1864,6 +1895,90 @@ public class NotificationManagerService extends SystemService { idOut[0] = id; } + /** + * Ensures that grouped notification receive their special treatment. + * + *

Cancels group children if the new notification causes a group to lose + * its summary.

+ * + *

Updates mSummaryByGroupKey.

+ */ + private void handleGroupedNotificationLocked(NotificationRecord r, NotificationRecord old, + int callingUid, int callingPid) { + StatusBarNotification sbn = r.sbn; + Notification n = sbn.getNotification(); + String group = sbn.getGroupKey(); + boolean isSummary = n.isGroupSummary(); + + Notification oldN = old != null ? old.sbn.getNotification() : null; + String oldGroup = old != null ? old.sbn.getGroupKey() : null; + boolean oldIsSummary = old != null && oldN.isGroupSummary(); + + if (oldIsSummary) { + NotificationRecord removedSummary = mSummaryByGroupKey.remove(oldGroup); + if (removedSummary != old) { + String removedKey = + removedSummary != null ? removedSummary.getKey() : ""; + Slog.w(TAG, "Removed summary didn't match old notification: old=" + old.getKey() + + ", removed=" + removedKey); + } + } + if (isSummary) { + mSummaryByGroupKey.put(group, r); + } + + // Clear out group children of the old notification if the update + // causes the group summary to go away. This happens when the old + // notification was a summary and the new one isn't, or when the old + // notification was a summary and its group key changed. + if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) { + cancelGroupChildrenLocked(old, callingUid, callingPid, null, + REASON_GROUP_SUMMARY_CANCELED); + } + } + + /** + * Performs group notification optimizations if SysUI is the only active + * notification listener and returns whether the given notification should + * be ignored. + * + *

Returns true if the given notification is a child of a group with a + * summary, which means that SysUI will never show it, and hence the new + * notification can be safely ignored.

+ * + *

For summaries, cancels all children of that group, as SysUI will + * never show them anymore.

+ * + * @return true if the given notification can be ignored as an optimization + */ + private boolean removeUnusedGroupedNotificationLocked(NotificationRecord r, + int callingUid, int callingPid) { + // No optimizations are possible if listeners want groups. + if (mListeners.notificationGroupsDesired()) { + return false; + } + + StatusBarNotification sbn = r.sbn; + String group = sbn.getGroupKey(); + boolean isSummary = sbn.getNotification().isGroupSummary(); + boolean isChild = sbn.getNotification().isGroupChild(); + + NotificationRecord summary = mSummaryByGroupKey.get(group); + if (isChild && summary != null) { + // Child with an active summary -> ignore + if (DBG) { + Slog.d(TAG, "Ignoring group child " + sbn.getKey() + " due to existing summary " + + summary.getKey()); + } + return true; + } else if (isSummary) { + // Summary -> cancel children + cancelGroupChildrenLocked(r, callingUid, callingPid, null, + REASON_GROUP_OPTIMIZATION); + } + return false; + } + private void buzzBeepBlinkLocked(NotificationRecord record) { boolean buzzBeepBlinked = false; final Notification notification = record.sbn.getNotification(); @@ -2386,6 +2501,11 @@ public class NotificationManagerService extends SystemService { } mNotificationsByKey.remove(r.sbn.getKey()); + String groupKey = r.getGroupKey(); + NotificationRecord groupSummary = mSummaryByGroupKey.get(groupKey); + if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) { + mSummaryByGroupKey.remove(groupKey); + } // Save it for users of getHistoricalNotifications() mArchive.record(r.sbn); @@ -2433,7 +2553,8 @@ public class NotificationManagerService extends SystemService { mNotificationList.remove(index); cancelNotificationLocked(r, sendDelete, reason); - cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName); + cancelGroupChildrenLocked(r, callingUid, callingPid, listenerName, + REASON_GROUP_SUMMARY_CANCELED); updateLightsLocked(); } } @@ -2512,7 +2633,7 @@ public class NotificationManagerService extends SystemService { final int M = canceledNotifications.size(); for (int i = 0; i < M; i++) { cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, - listenerName); + listenerName, REASON_GROUP_SUMMARY_CANCELED); } } if (canceledNotifications != null) { @@ -2556,14 +2677,14 @@ public class NotificationManagerService extends SystemService { int M = canceledNotifications != null ? canceledNotifications.size() : 0; for (int i = 0; i < M; i++) { cancelGroupChildrenLocked(canceledNotifications.get(i), callingUid, callingPid, - listenerName); + listenerName, REASON_GROUP_SUMMARY_CANCELED); } updateLightsLocked(); } // Warning: The caller is responsible for invoking updateLightsLocked(). private void cancelGroupChildrenLocked(NotificationRecord r, int callingUid, int callingPid, - String listenerName) { + String listenerName, int reason) { Notification n = r.getNotification(); if (!n.isGroupSummary()) { return; @@ -2583,11 +2704,10 @@ public class NotificationManagerService extends SystemService { StatusBarNotification childSbn = childR.sbn; if (childR.getNotification().isGroupChild() && childR.getGroupKey().equals(r.getGroupKey())) { - EventLogTags.writeNotificationCancel(callingUid, callingPid, - pkg, childSbn.getId(), childSbn.getTag(), userId, 0, 0, - REASON_GROUP_SUMMARY_CANCELED, listenerName); + EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(), + childSbn.getTag(), userId, 0, 0, reason, listenerName); mNotificationList.remove(i); - cancelNotificationLocked(childR, false, REASON_GROUP_SUMMARY_CANCELED); + cancelNotificationLocked(childR, false, reason); } } } @@ -2783,6 +2903,7 @@ public class NotificationManagerService extends SystemService { public class NotificationListeners extends ManagedServices { private final ArraySet mLightTrimListeners = new ArraySet<>(); + private boolean mNotificationGroupsDesired; public NotificationListeners() { super(getContext(), mHandler, mNotificationList, mUserProfiles); @@ -2810,6 +2931,7 @@ public class NotificationManagerService extends SystemService { final INotificationListener listener = (INotificationListener) info.service; final NotificationRankingUpdate update; synchronized (mNotificationList) { + updateNotificationGroupsDesiredLocked(); update = makeRankingUpdateLocked(info); } try { @@ -2825,6 +2947,7 @@ public class NotificationManagerService extends SystemService { updateListenerHintsLocked(); } mLightTrimListeners.remove(removed); + updateNotificationGroupsDesiredLocked(); } public void setOnNotificationPostedTrimLocked(ManagedServiceInfo info, int trim) { @@ -3028,6 +3151,31 @@ public class NotificationManagerService extends SystemService { } return false; } + + /** + * Returns whether any of the currently registered listeners wants to receive notification + * groups. + * + *

Currently we assume groups are desired by non-SystemUI listeners.

+ */ + public boolean notificationGroupsDesired() { + return mNotificationGroupsDesired; + } + + private void updateNotificationGroupsDesiredLocked() { + mNotificationGroupsDesired = true; + // No listeners, no groups. + if (mServices.isEmpty()) { + mNotificationGroupsDesired = false; + return; + } + // One listener: Check whether it's SysUI. + if (mServices.size() == 1 && + mServices.get(0).component.getPackageName().equals("com.android.systemui")) { + mNotificationGroupsDesired = false; + return; + } + } } public static final class DumpFilter {