Merge "Move autogrouping into framework."
diff --git a/api/system-current.txt b/api/system-current.txt
index 7805ae3..573b1f2 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -37556,8 +37556,6 @@
method public int getUser();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
- field public static final java.lang.String GROUP_KEY_OVERRIDE_KEY = "group_key_override";
- field public static final java.lang.String NEEDS_AUTOGROUPING_KEY = "autogroup_needed";
}
public final class Condition implements android.os.Parcelable {
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 530b8bb..4150172 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -134,11 +134,11 @@
this.mLockscreenVisibility = lockscreenVisibility;
}
- // Modifiable by apps.
+ // Modifiable by apps on channel creation.
/**
* Sets the ringtone that should be played for notifications posted to this channel if
- * the notifications don't supply a ringtone.
+ * the notifications don't supply a ringtone. Only modifiable on channel creation.
*/
public void setDefaultRingtone(Uri defaultRingtone) {
this.mRingtone = defaultRingtone;
@@ -146,7 +146,7 @@
/**
* Sets whether notifications posted to this channel should display notification lights,
- * on devices that support that feature.
+ * on devices that support that feature. Only modifiable on channel creation.
*/
public void setLights(boolean lights) {
this.mLights = lights;
@@ -154,7 +154,7 @@
/**
* Sets whether notification posted to this channel should vibrate, even if individual
- * notifications are marked as having vibration.
+ * notifications are marked as having vibration only modifiable on channel creation.
*/
public void setVibration(boolean vibration) {
this.mVibration = vibration;
diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java
index 61d0a1e..4a956c6 100644
--- a/core/java/android/service/notification/Adjustment.java
+++ b/core/java/android/service/notification/Adjustment.java
@@ -36,9 +36,6 @@
private final Bundle mSignals;
private final int mUser;
- public static final String GROUP_KEY_OVERRIDE_KEY = "group_key_override";
- public static final String NEEDS_AUTOGROUPING_KEY = "autogroup_needed";
-
/**
* Create a notification adjustment.
*
diff --git a/packages/ExtServices/src/android/ext/services/notification/Ranker.java b/packages/ExtServices/src/android/ext/services/notification/Ranker.java
index 2ce667c..63fc157 100644
--- a/packages/ExtServices/src/android/ext/services/notification/Ranker.java
+++ b/packages/ExtServices/src/android/ext/services/notification/Ranker.java
@@ -35,180 +35,15 @@
import android.ext.services.R;
/**
- * Class that provides an updatable ranker module for the notification manager..
+ * Class that provides an updatable ranker module for the notification manager.
*/
public final class Ranker extends NotificationRankerService {
private static final String TAG = "RocketRanker";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private static final int AUTOBUNDLE_AT_COUNT = 4;
- private static final String AUTOBUNDLE_KEY = "ranker_bundle";
-
- // Map of user : <Map of package : notification keys>. Only contains notifications that are not
- // bundled by the app (aka no group or sort key).
- Map<Integer, Map<String, LinkedHashSet<String>>> mUnbundledNotifications;
-
@Override
public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance,
boolean user) {
- if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey());
return null;
}
-
- @Override
- public void onNotificationPosted(StatusBarNotification sbn) {
- if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
- try {
- List<String> notificationsToBundle = new ArrayList<>();
- if (!sbn.isAppGroup()) {
- // Not grouped by the app, add to the list of notifications for the app;
- // send bundling update if app exceeds the autobundling limit.
- synchronized (mUnbundledNotifications) {
- Map<String, LinkedHashSet<String>> unbundledNotificationsByUser
- = mUnbundledNotifications.get(sbn.getUserId());
- if (unbundledNotificationsByUser == null) {
- unbundledNotificationsByUser = new HashMap<>();
- }
- mUnbundledNotifications.put(sbn.getUserId(), unbundledNotificationsByUser);
- LinkedHashSet<String> notificationsForPackage
- = unbundledNotificationsByUser.get(sbn.getPackageName());
- if (notificationsForPackage == null) {
- notificationsForPackage = new LinkedHashSet<>();
- }
-
- notificationsForPackage.add(sbn.getKey());
- unbundledNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage);
-
- if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) {
- for (String key : notificationsForPackage) {
- notificationsToBundle.add(key);
- }
- }
- }
- if (notificationsToBundle.size() > 0) {
- adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0),
- true, sbn.getUserId());
- adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true,
- sbn.getUserId());
- }
- } else {
- // Grouped, but not by us. Send updates to unautobundle, if we bundled it.
- maybeUnbundle(sbn, false, sbn.getUserId());
- }
- } catch (Exception e) {
- Slog.e(TAG, "Failure processing new notification", e);
- }
- }
-
- @Override
- public void onNotificationRemoved(StatusBarNotification sbn) {
- try {
- maybeUnbundle(sbn, true, sbn.getUserId());
- } catch (Exception e) {
- Slog.e(TAG, "Error processing canceled notification", e);
- }
- }
-
- /**
- * Un-autobundles notifications that are now grouped by the app. Additionally cancels
- * autobundling if the status change of this notification resulted in the loose notification
- * count being under the limit.
- */
- private void maybeUnbundle(StatusBarNotification sbn, boolean notificationGone, int user) {
- List<String> notificationsToUnAutobundle = new ArrayList<>();
- boolean removeSummary = false;
- synchronized (mUnbundledNotifications) {
- Map<String, LinkedHashSet<String>> unbundledNotificationsByUser
- = mUnbundledNotifications.get(sbn.getUserId());
- if (unbundledNotificationsByUser == null || unbundledNotificationsByUser.size() == 0) {
- return;
- }
- LinkedHashSet<String> notificationsForPackage
- = unbundledNotificationsByUser.get(sbn.getPackageName());
- if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
- return;
- }
- if (notificationsForPackage.remove(sbn.getKey())) {
- if (!notificationGone) {
- // Add the current notification to the unbundling list if it still exists.
- notificationsToUnAutobundle.add(sbn.getKey());
- }
- // If the status change of this notification has brought the number of loose
- // notifications back below the limit, remove the summary and un-autobundle.
- if (notificationsForPackage.size() == AUTOBUNDLE_AT_COUNT - 1) {
- removeSummary = true;
- for (String key : notificationsForPackage) {
- notificationsToUnAutobundle.add(key);
- }
- }
- }
- }
- if (notificationsToUnAutobundle.size() > 0) {
- if (removeSummary) {
- adjustAutobundlingSummary(sbn.getPackageName(), null, false, user);
- }
- adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false,
- user);
- }
- }
-
- @Override
- public void onListenerConnected() {
- if (DEBUG) Log.i(TAG, "CONNECTED");
- mUnbundledNotifications = new HashMap<>();
- for (StatusBarNotification sbn : getActiveNotifications()) {
- onNotificationPosted(sbn);
- }
- }
-
- private void adjustAutobundlingSummary(String packageName, String key, boolean summaryNeeded,
- int user) {
- Bundle signals = new Bundle();
- if (summaryNeeded) {
- signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, true);
- signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
- } else {
- signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false);
- }
- Adjustment adjustment = new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
- getContext().getString(R.string.notification_ranker_autobundle_explanation), null,
- user);
- if (DEBUG) {
- Log.i(TAG, "Summary update for: " + packageName + " "
- + (summaryNeeded ? "adding" : "removing"));
- }
- try {
- adjustNotification(adjustment);
- } catch (Exception e) {
- Slog.e(TAG, "Adjustment failed", e);
- }
-
- }
- private void adjustNotificationBundling(String packageName, List<String> keys, boolean bundle,
- int user) {
- List<Adjustment> adjustments = new ArrayList<>();
- for (String key : keys) {
- adjustments.add(createBundlingAdjustment(packageName, key, bundle, user));
- if (DEBUG) Log.i(TAG, "Sending bundling adjustment for: " + key);
- }
- try {
- adjustNotifications(adjustments);
- } catch (Exception e) {
- Slog.e(TAG, "Adjustments failed", e);
- }
- }
-
- private Adjustment createBundlingAdjustment(String packageName, String key, boolean bundle,
- int user) {
- Bundle signals = new Bundle();
- if (bundle) {
- signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
- } else {
- signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
- }
- return new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
- getContext().getString(R.string.notification_ranker_autobundle_explanation),
- null, user);
- }
-
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
new file mode 100644
index 0000000..8ea4909
--- /dev/null
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * NotificationManagerService helper for auto-grouping notifications.
+ */
+public class GroupHelper {
+ private static final String TAG = "GroupHelper";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ protected static final int AUTOGROUP_AT_COUNT = 4;
+ protected static final String AUTOGROUP_KEY = "ranker_group";
+
+ private final Callback mCallback;
+
+ // Map of user : <Map of package : notification keys>. Only contains notifications that are not
+ // groupd by the app (aka no group or sort key).
+ Map<Integer, Map<String, LinkedHashSet<String>>> mUngroupedNotifications = new HashMap<>();
+
+ public GroupHelper(Callback callback) {;
+ mCallback = callback;
+ }
+
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
+ try {
+ List<String> notificationsToGroup = new ArrayList<>();
+ if (!sbn.isAppGroup()) {
+ // Not grouped by the app, add to the list of notifications for the app;
+ // send grouping update if app exceeds the autogrouping limit.
+ synchronized (mUngroupedNotifications) {
+ Map<String, LinkedHashSet<String>> ungroupedNotificationsByUser
+ = mUngroupedNotifications.get(sbn.getUserId());
+ if (ungroupedNotificationsByUser == null) {
+ ungroupedNotificationsByUser = new HashMap<>();
+ }
+ mUngroupedNotifications.put(sbn.getUserId(), ungroupedNotificationsByUser);
+ LinkedHashSet<String> notificationsForPackage
+ = ungroupedNotificationsByUser.get(sbn.getPackageName());
+ if (notificationsForPackage == null) {
+ notificationsForPackage = new LinkedHashSet<>();
+ }
+
+ notificationsForPackage.add(sbn.getKey());
+ ungroupedNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage);
+
+ if (notificationsForPackage.size() >= AUTOGROUP_AT_COUNT) {
+ notificationsToGroup.addAll(notificationsForPackage);
+ }
+ }
+ if (notificationsToGroup.size() > 0) {
+ adjustAutogroupingSummary(sbn.getUserId(), sbn.getPackageName(),
+ notificationsToGroup.get(0), true);
+ adjustNotificationBundling(notificationsToGroup, true);
+ }
+ } else {
+ // Grouped, but not by us. Send updates to un-autogroup, if we grouped it.
+ maybeUngroup(sbn, false, sbn.getUserId());
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Failure processing new notification", e);
+ }
+ }
+
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+ try {
+ maybeUngroup(sbn, true, sbn.getUserId());
+ } catch (Exception e) {
+ Slog.e(TAG, "Error processing canceled notification", e);
+ }
+ }
+
+ /**
+ * Un-autogroups notifications that are now grouped by the app. Additionally cancels
+ * autogrouping if the status change of this notification resulted in the loose notification
+ * count being under the limit.
+ */
+ private void maybeUngroup(StatusBarNotification sbn, boolean notificationGone, int userId) {
+ List<String> notificationsToUnAutogroup = new ArrayList<>();
+ boolean removeSummary = false;
+ synchronized (mUngroupedNotifications) {
+ Map<String, LinkedHashSet<String>> ungroupdNotificationsByUser
+ = mUngroupedNotifications.get(sbn.getUserId());
+ if (ungroupdNotificationsByUser == null || ungroupdNotificationsByUser.size() == 0) {
+ return;
+ }
+ LinkedHashSet<String> notificationsForPackage
+ = ungroupdNotificationsByUser.get(sbn.getPackageName());
+ if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
+ return;
+ }
+ if (notificationsForPackage.remove(sbn.getKey())) {
+ if (!notificationGone) {
+ // Add the current notification to the ungrouping list if it still exists.
+ notificationsToUnAutogroup.add(sbn.getKey());
+ }
+ // If the status change of this notification has brought the number of loose
+ // notifications back below the limit, remove the summary and un-autogroup.
+ if (notificationsForPackage.size() == AUTOGROUP_AT_COUNT - 1) {
+ removeSummary = true;
+ for (String key : notificationsForPackage) {
+ notificationsToUnAutogroup.add(key);
+ }
+ }
+ }
+ }
+ if (notificationsToUnAutogroup.size() > 0) {
+ if (removeSummary) {
+ adjustAutogroupingSummary(userId, sbn.getPackageName(), null, false);
+ }
+ adjustNotificationBundling(notificationsToUnAutogroup, false);
+ }
+ }
+
+ private void adjustAutogroupingSummary(int userId, String packageName, String triggeringKey,
+ boolean summaryNeeded) {
+ if (summaryNeeded) {
+ mCallback.addAutoGroupSummary(userId, packageName, triggeringKey);
+ } else {
+ mCallback.removeAutoGroupSummary(userId, packageName);
+ }
+ }
+
+ private void adjustNotificationBundling(List<String> keys, boolean group) {
+ for (String key : keys) {
+ if (DEBUG) Log.i(TAG, "Sending grouping adjustment for: " + key + " group? " + group);
+ if (group) {
+ mCallback.addAutoGroup(key);
+ } else {
+ mCallback.removeAutoGroup(key);
+ }
+ }
+ }
+
+ protected interface Callback {
+ void addAutoGroup(String key);
+ void removeAutoGroup(String key);
+ void addAutoGroupSummary(int userId, String pkg, String triggeringKey);
+ void removeAutoGroupSummary(int user, String pkg);
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 61bf3bd..4e50567 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -311,6 +311,7 @@
private String mSystemNotificationSound;
private SnoozeHelper mSnoozeHelper;
+ private GroupHelper mGroupHelper;
private static class Archive {
final int mBufferSize;
@@ -1017,6 +1018,35 @@
}
}
}, mUserProfiles);
+ mGroupHelper = new GroupHelper(new GroupHelper.Callback() {
+ @Override
+ public void addAutoGroup(String key) {
+ synchronized (mNotificationList) {
+ addAutogroupKeyLocked(key);
+ }
+ mRankingHandler.requestSort();
+ }
+
+ @Override
+ public void removeAutoGroup(String key) {
+ synchronized (mNotificationList) {
+ removeAutogroupKeyLocked(key);
+ }
+ mRankingHandler.requestSort();
+ }
+
+ @Override
+ public void addAutoGroupSummary(int userId, String pkg, String triggeringKey) {
+ createAutoGroupSummary(userId, pkg, triggeringKey);
+ }
+
+ @Override
+ public void removeAutoGroupSummary(int userId, String pkg) {
+ synchronized (mNotificationList) {
+ clearAutogroupSummaryLocked(userId, pkg);
+ }
+ }
+ });
final File systemDir = new File(Environment.getDataDirectory(), "system");
mPolicyFile = new AtomicFile(new File(systemDir, "notification_policy.xml"));
@@ -2350,7 +2380,6 @@
mRankerServices.checkServiceTokenLocked(token);
applyAdjustmentLocked(adjustment);
}
- maybeAddAutobundleSummary(adjustment);
mRankingHandler.requestSort();
} finally {
Binder.restoreCallingIdentity(identity);
@@ -2369,9 +2398,6 @@
applyAdjustmentLocked(adjustment);
}
}
- for (Adjustment adjustment : adjustments) {
- maybeAddAutobundleSummary(adjustment);
- }
mRankingHandler.requestSort();
} finally {
Binder.restoreCallingIdentity(identity);
@@ -2380,7 +2406,6 @@
};
private void applyAdjustmentLocked(Adjustment adjustment) {
- maybeClearAutobundleSummaryLocked(adjustment);
NotificationRecord n = mNotificationsByKey.get(adjustment.getKey());
if (n == null) {
return;
@@ -2390,107 +2415,97 @@
}
if (adjustment.getSignals() != null) {
Bundle.setDefusable(adjustment.getSignals(), true);
- final String autoGroupKey = adjustment.getSignals().getString(
- Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
- if (autoGroupKey == null) {
- EventLogTags.writeNotificationUnautogrouped(adjustment.getKey());
- } else {
- EventLogTags.writeNotificationAutogrouped(adjustment.getKey());
- }
- n.sbn.setOverrideGroupKey(autoGroupKey);
+ // TODO: apply signals
}
}
- // Clears the 'fake' auto-bunding summary.
- private void maybeClearAutobundleSummaryLocked(Adjustment adjustment) {
- if (adjustment.getSignals() != null) {
- Bundle.setDefusable(adjustment.getSignals(), true);
- if (adjustment.getSignals().containsKey(Adjustment.NEEDS_AUTOGROUPING_KEY)
- && !adjustment.getSignals().getBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false)) {
- ArrayMap<String, String> summaries =
- mAutobundledSummaries.get(adjustment.getUser());
- if (summaries != null && summaries.containsKey(adjustment.getPackage())) {
- // Clear summary.
- final NotificationRecord removed = mNotificationsByKey.get(
- summaries.remove(adjustment.getPackage()));
- if (removed != null) {
- mNotificationList.remove(removed);
- cancelNotificationLocked(removed, false, REASON_UNAUTOBUNDLED);
- }
- }
+ private void addAutogroupKeyLocked(String key) {
+ NotificationRecord n = mNotificationsByKey.get(key);
+ if (n == null) {
+ return;
+ }
+ n.sbn.setOverrideGroupKey(GroupHelper.AUTOGROUP_KEY);
+ EventLogTags.writeNotificationAutogrouped(key);
+ }
+
+ private void removeAutogroupKeyLocked(String key) {
+ NotificationRecord n = mNotificationsByKey.get(key);
+ if (n == null) {
+ return;
+ }
+ n.sbn.setOverrideGroupKey(null);
+ EventLogTags.writeNotificationUnautogrouped(key);
+ }
+
+ // Clears the 'fake' auto-group summary.
+ private void clearAutogroupSummaryLocked(int userId, String pkg) {
+ ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
+ if (summaries != null && summaries.containsKey(pkg)) {
+ // Clear summary.
+ final NotificationRecord removed = mNotificationsByKey.get(summaries.remove(pkg));
+ if (removed != null) {
+ mNotificationList.remove(removed);
+ cancelNotificationLocked(removed, false, REASON_UNAUTOBUNDLED);
}
}
}
// Posts a 'fake' summary for a package that has exceeded the solo-notification limit.
- private void maybeAddAutobundleSummary(Adjustment adjustment) {
- if (adjustment.getSignals() != null) {
- Bundle.setDefusable(adjustment.getSignals(), true);
- if (adjustment.getSignals().getBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false)) {
- final String newAutoBundleKey =
- adjustment.getSignals().getString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
- int userId = -1;
- NotificationRecord summaryRecord = null;
- synchronized (mNotificationList) {
- NotificationRecord notificationRecord =
- mNotificationsByKey.get(adjustment.getKey());
- if (notificationRecord == null) {
- // The notification could have been cancelled again already. A successive
- // adjustment will post a summary if needed.
- return;
- }
- final StatusBarNotification adjustedSbn = notificationRecord.sbn;
- userId = adjustedSbn.getUser().getIdentifier();
- ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
- if (summaries == null) {
- summaries = new ArrayMap<>();
- }
- mAutobundledSummaries.put(userId, summaries);
- if (!summaries.containsKey(adjustment.getPackage())
- && newAutoBundleKey != null) {
- // Add summary
- final ApplicationInfo appInfo =
- adjustedSbn.getNotification().extras.getParcelable(
- Notification.EXTRA_BUILDER_APPLICATION_INFO);
- final Bundle extras = new Bundle();
- extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
- final Notification summaryNotification =
- new Notification.Builder(getContext()).setSmallIcon(
- adjustedSbn.getNotification().getSmallIcon())
- .setGroupSummary(true)
- .setGroup(newAutoBundleKey)
- .setFlag(Notification.FLAG_AUTOGROUP_SUMMARY, true)
- .setFlag(Notification.FLAG_GROUP_SUMMARY, true)
- .setColor(adjustedSbn.getNotification().color)
- .setLocalOnly(true)
- .build();
- summaryNotification.extras.putAll(extras);
- Intent appIntent = getContext().getPackageManager()
- .getLaunchIntentForPackage(adjustment.getPackage());
- if (appIntent != null) {
- summaryNotification.contentIntent = PendingIntent.getActivityAsUser(
- getContext(), 0, appIntent, 0, null,
- UserHandle.of(userId));
- }
- final StatusBarNotification summarySbn =
- new StatusBarNotification(adjustedSbn.getPackageName(),
- adjustedSbn.getOpPkg(),
- Integer.MAX_VALUE, Adjustment.GROUP_KEY_OVERRIDE_KEY,
- adjustedSbn.getUid(), adjustedSbn.getInitialPid(),
- summaryNotification, adjustedSbn.getUser(),
- newAutoBundleKey,
- System.currentTimeMillis());
- summaryRecord = new NotificationRecord(getContext(), summarySbn,
- mRankingHelper.getNotificationChannel(adjustedSbn.getPackageName(),
- adjustedSbn.getUid(),
- adjustedSbn.getNotification().getNotificationChannel()));
- summaries.put(adjustment.getPackage(), summarySbn.getKey());
- }
- }
- if (summaryRecord != null) {
- mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord));
- }
+ private void createAutoGroupSummary(int userId, String pkg, String triggeringKey) {
+ NotificationRecord summaryRecord = null;
+ synchronized (mNotificationList) {
+ NotificationRecord notificationRecord = mNotificationsByKey.get(triggeringKey);
+ if (notificationRecord == null) {
+ // The notification could have been cancelled again already. A successive
+ // adjustment will post a summary if needed.
+ return;
}
+ final StatusBarNotification adjustedSbn = notificationRecord.sbn;
+ userId = adjustedSbn.getUser().getIdentifier();
+ ArrayMap<String, String> summaries = mAutobundledSummaries.get(userId);
+ if (summaries == null) {
+ summaries = new ArrayMap<>();
+ }
+ mAutobundledSummaries.put(userId, summaries);
+ if (!summaries.containsKey(pkg)) {
+ // Add summary
+ final ApplicationInfo appInfo =
+ adjustedSbn.getNotification().extras.getParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO);
+ final Bundle extras = new Bundle();
+ extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, appInfo);
+ final Notification summaryNotification =
+ new Notification.Builder(getContext()).setSmallIcon(
+ adjustedSbn.getNotification().getSmallIcon())
+ .setGroupSummary(true)
+ .setGroup(GroupHelper.AUTOGROUP_KEY)
+ .setFlag(Notification.FLAG_AUTOGROUP_SUMMARY, true)
+ .setFlag(Notification.FLAG_GROUP_SUMMARY, true)
+ .setColor(adjustedSbn.getNotification().color)
+ .setLocalOnly(true)
+ .build();
+ summaryNotification.extras.putAll(extras);
+ Intent appIntent = getContext().getPackageManager().getLaunchIntentForPackage(pkg);
+ if (appIntent != null) {
+ summaryNotification.contentIntent = PendingIntent.getActivityAsUser(
+ getContext(), 0, appIntent, 0, null, UserHandle.of(userId));
+ }
+ final StatusBarNotification summarySbn =
+ new StatusBarNotification(adjustedSbn.getPackageName(),
+ adjustedSbn.getOpPkg(), Integer.MAX_VALUE,
+ GroupHelper.AUTOGROUP_KEY, adjustedSbn.getUid(),
+ adjustedSbn.getInitialPid(), summaryNotification,
+ adjustedSbn.getUser(), GroupHelper.AUTOGROUP_KEY,
+ System.currentTimeMillis());
+ summaryRecord = new NotificationRecord(getContext(), summarySbn,
+ mRankingHelper.getNotificationChannel(adjustedSbn.getPackageName(),
+ adjustedSbn.getUid(),
+ adjustedSbn.getNotification().getNotificationChannel()));
+ summaries.put(pkg, summarySbn.getKey());
+ }
+ }
+ if (summaryRecord != null) {
+ mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord));
}
}
@@ -2690,6 +2705,12 @@
(r.mOriginalFlags & ~Notification.FLAG_FOREGROUND_SERVICE);
mRankingHelper.sort(mNotificationList);
mListeners.notifyPostedLocked(sbn, sbn /* oldSbn */);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mGroupHelper.onNotificationPosted(sbn);
+ }
+ });
}
}
};
@@ -2914,10 +2935,22 @@
if (notification.getSmallIcon() != null) {
StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
mListeners.notifyPostedLocked(n, oldSbn);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mGroupHelper.onNotificationPosted(n);
+ }
+ });
} else {
Slog.e(TAG, "Not posting notification without small icon: " + notification);
if (old != null && !old.isCanceled) {
mListeners.notifyRemovedLocked(n);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mGroupHelper.onNotificationRemoved(n);
+ }
+ });
}
// ATTENTION: in a future release we will bail out here
// so that we do not play sounds, show lights, etc. for invalid
@@ -3525,6 +3558,12 @@
if (r.getNotification().getSmallIcon() != null) {
r.isCanceled = true;
mListeners.notifyRemovedLocked(r.sbn);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mGroupHelper.onNotificationRemoved(r.sbn);
+ }
+ });
}
final String canceledKey = r.getKey();
diff --git a/services/tests/servicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/servicestests/src/com/android/server/notification/GroupHelperTest.java
new file mode 100644
index 0000000..22b674b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GroupHelperTest {
+ private @Mock GroupHelper.Callback mCallback;
+
+ private GroupHelper mGroupHelper;
+
+ private Context getContext() {
+ return InstrumentationRegistry.getTargetContext();
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mGroupHelper = new GroupHelper(mCallback);
+ }
+
+ private StatusBarNotification getSbn(String pkg, int id, String tag,
+ UserHandle user, String groupKey) {
+ Notification.Builder nb = new Notification.Builder(getContext())
+ .setContentTitle("A")
+ .setWhen(1205);
+ if (groupKey != null) {
+ nb.setGroup(groupKey);
+ }
+ return new StatusBarNotification(pkg, pkg, id, tag, 0, 0, 0, nb.build(), user);
+ }
+
+ private StatusBarNotification getSbn(String pkg, int id, String tag,
+ UserHandle user) {
+ return getSbn(pkg, id, tag, user, null);
+ }
+
+ @Test
+ public void testNoGroup_postingUnderLimit() throws Exception {
+ final String pkg = "package";
+ for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+ mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+ }
+ verify(mCallback, never()).addAutoGroupSummary(
+ eq(UserHandle.USER_SYSTEM), eq(pkg), anyString());
+ verify(mCallback, never()).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ }
+
+ @Test
+ public void testNoGroup_multiPackage() throws Exception {
+ final String pkg = "package";
+ final String pkg2 = "package2";
+ for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+ mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+ }
+ mGroupHelper.onNotificationPosted(
+ getSbn(pkg2, GroupHelper.AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM));
+ verify(mCallback, never()).addAutoGroupSummary(
+ eq(UserHandle.USER_SYSTEM), eq(pkg), anyString());
+ verify(mCallback, never()).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ }
+
+ @Test
+ public void testNoGroup_multiUser() throws Exception {
+ final String pkg = "package";
+ for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+ mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+ }
+ mGroupHelper.onNotificationPosted(
+ getSbn(pkg, GroupHelper.AUTOGROUP_AT_COUNT, "four", UserHandle.ALL));
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), eq(pkg), anyString());
+ verify(mCallback, never()).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ }
+
+ @Test
+ public void testNoGroup_someAreGrouped() throws Exception {
+ final String pkg = "package";
+ for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT - 1; i++) {
+ mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+ }
+ mGroupHelper.onNotificationPosted(
+ getSbn(pkg, GroupHelper.AUTOGROUP_AT_COUNT, "four", UserHandle.SYSTEM, "a"));
+ verify(mCallback, never()).addAutoGroupSummary(
+ eq(UserHandle.USER_SYSTEM), eq(pkg), anyString());
+ verify(mCallback, never()).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ }
+
+
+ @Test
+ public void testPostingOverLimit() throws Exception {
+ final String pkg = "package";
+ for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT; i++) {
+ mGroupHelper.onNotificationPosted(getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM));
+ }
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString());
+ verify(mCallback, times(GroupHelper.AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+ }
+
+ @Test
+ public void testDropBelowLimitRemoveGroup() throws Exception {
+ final String pkg = "package";
+ List<StatusBarNotification> posted = new ArrayList<>();
+ for (int i = 0; i < GroupHelper.AUTOGROUP_AT_COUNT; i++) {
+ final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+ posted.add(sbn);
+ mGroupHelper.onNotificationPosted(sbn);
+ }
+ mGroupHelper.onNotificationRemoved(posted.remove(0));
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString());
+ verify(mCallback, times(GroupHelper.AUTOGROUP_AT_COUNT)).addAutoGroup(anyString());
+ verify(mCallback, times(GroupHelper.AUTOGROUP_AT_COUNT - 1)).removeAutoGroup(anyString());
+ verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+ }
+}