diff options
author | Julia Reynolds <juliacr@google.com> | 2016-03-17 11:05:58 -0400 |
---|---|---|
committer | Julia Reynolds <juliacr@google.com> | 2016-04-08 13:55:03 -0400 |
commit | e46bb37acf6d3cfb9974672ace93f5381f70ad99 (patch) | |
tree | 3a4108622a23800cdda3c7e9dab839044406ebdc | |
parent | 5c1fbadb1a1169186b7a0a357ae9c35183258c6c (diff) |
Allow the ranker to autobundle notifications.
Bug: 26709317
Change-Id: I5017a471604e513a7b582cdc2f3472441e108fae
18 files changed, 888 insertions, 117 deletions
diff --git a/api/current.txt b/api/current.txt index 1c4f85bc7abe..8a30ca51c855 100644 --- a/api/current.txt +++ b/api/current.txt @@ -34739,6 +34739,7 @@ package android.service.notification { method public int getImportance(); method public java.lang.CharSequence getImportanceExplanation(); method public java.lang.String getKey(); + method public java.lang.String getOverrideGroupKey(); method public int getRank(); method public int getSuppressedVisualEffects(); method public boolean isAmbient(); @@ -34769,13 +34770,16 @@ package android.service.notification { method public int getId(); method public java.lang.String getKey(); method public android.app.Notification getNotification(); + method public java.lang.String getOverrideGroupKey(); method public java.lang.String getPackageName(); method public long getPostTime(); method public java.lang.String getTag(); method public android.os.UserHandle getUser(); method public deprecated int getUserId(); method public boolean isClearable(); + method public boolean isGroup(); method public boolean isOngoing(); + method public void setOverrideGroupKey(java.lang.String); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR; } diff --git a/api/system-current.txt b/api/system-current.txt index 039b9adc24a2..5d6d3ea6a65a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5076,6 +5076,7 @@ package android.app { field public static final java.lang.String EXTRA_THREAD_TITLE = "android.threadTitle"; field public static final java.lang.String EXTRA_TITLE = "android.title"; field public static final java.lang.String EXTRA_TITLE_BIG = "android.title.big"; + field public static final int FLAG_AUTOGROUP_SUMMARY = 1024; // 0x400 field public static final int FLAG_AUTO_CANCEL = 16; // 0x10 field public static final int FLAG_FOREGROUND_SERVICE = 64; // 0x40 field public static final int FLAG_GROUP_SUMMARY = 512; // 0x200 @@ -37117,6 +37118,22 @@ package android.service.media { package android.service.notification { + public final class Adjustment implements android.os.Parcelable { + ctor public Adjustment(java.lang.String, java.lang.String, int, android.os.Bundle, java.lang.CharSequence, android.net.Uri); + ctor protected Adjustment(android.os.Parcel); + method public int describeContents(); + method public java.lang.CharSequence getExplanation(); + method public int getImportance(); + method public java.lang.String getKey(); + method public java.lang.String getPackage(); + method public android.net.Uri getReference(); + method public android.os.Bundle getSignals(); + 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 class Condition implements android.os.Parcelable { ctor public Condition(android.net.Uri, java.lang.String, int); ctor public Condition(android.net.Uri, java.lang.String, java.lang.String, java.lang.String, int, int, int); @@ -37210,6 +37227,7 @@ package android.service.notification { method public int getImportance(); method public java.lang.CharSequence getImportanceExplanation(); method public java.lang.String getKey(); + method public java.lang.String getOverrideGroupKey(); method public int getRank(); method public int getSuppressedVisualEffects(); method public boolean isAmbient(); @@ -37233,11 +37251,12 @@ package android.service.notification { public abstract class NotificationRankerService extends android.service.notification.NotificationListenerService { ctor public NotificationRankerService(); - method public final void adjustImportance(java.lang.String, android.service.notification.NotificationRankerService.Adjustment); + method public final void adjustNotification(android.service.notification.Adjustment); + method public final void adjustNotifications(java.util.List<android.service.notification.Adjustment>); method public final android.os.IBinder onBind(android.content.Intent); method public void onNotificationActionClick(java.lang.String, long, int); method public void onNotificationClick(java.lang.String, long); - method public abstract android.service.notification.NotificationRankerService.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean); + method public abstract android.service.notification.Adjustment onNotificationEnqueued(android.service.notification.StatusBarNotification, int, boolean); method public void onNotificationRemoved(java.lang.String, long, int); method public void onNotificationVisibilityChanged(java.lang.String, long, boolean); field public static final int REASON_APP_CANCEL = 8; // 0x8 @@ -37254,14 +37273,11 @@ package android.service.notification { field public static final int REASON_PACKAGE_CHANGED = 5; // 0x5 field public static final int REASON_PACKAGE_SUSPENDED = 14; // 0xe field public static final int REASON_PROFILE_TURNED_OFF = 15; // 0xf + field public static final int REASON_UNAUTOBUNDLED = 16; // 0x10 field public static final int REASON_USER_STOPPED = 6; // 0x6 field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationRankerService"; } - public class NotificationRankerService.Adjustment { - ctor public NotificationRankerService.Adjustment(int, java.lang.CharSequence, android.net.Uri); - } - public class StatusBarNotification implements android.os.Parcelable { ctor public StatusBarNotification(java.lang.String, java.lang.String, int, java.lang.String, int, int, int, android.app.Notification, android.os.UserHandle, long); ctor public StatusBarNotification(android.os.Parcel); @@ -37271,13 +37287,16 @@ package android.service.notification { method public int getId(); method public java.lang.String getKey(); method public android.app.Notification getNotification(); + method public java.lang.String getOverrideGroupKey(); method public java.lang.String getPackageName(); method public long getPostTime(); method public java.lang.String getTag(); method public android.os.UserHandle getUser(); method public deprecated int getUserId(); method public boolean isClearable(); + method public boolean isGroup(); method public boolean isOngoing(); + method public void setOverrideGroupKey(java.lang.String); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR; } diff --git a/api/test-current.txt b/api/test-current.txt index 3febda193f3d..6b452fa0653d 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -34812,6 +34812,7 @@ package android.service.notification { method public int getImportance(); method public java.lang.CharSequence getImportanceExplanation(); method public java.lang.String getKey(); + method public java.lang.String getOverrideGroupKey(); method public int getRank(); method public int getSuppressedVisualEffects(); method public boolean isAmbient(); @@ -34842,13 +34843,16 @@ package android.service.notification { method public int getId(); method public java.lang.String getKey(); method public android.app.Notification getNotification(); + method public java.lang.String getOverrideGroupKey(); method public java.lang.String getPackageName(); method public long getPostTime(); method public java.lang.String getTag(); method public android.os.UserHandle getUser(); method public deprecated int getUserId(); method public boolean isClearable(); + method public boolean isGroup(); method public boolean isOngoing(); + method public void setOverrideGroupKey(java.lang.String); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.service.notification.StatusBarNotification> CREATOR; } diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 7a69c6234eb9..ee80ec344779 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.pm.ParceledListSlice; import android.net.Uri; import android.os.Bundle; +import android.service.notification.Adjustment; import android.service.notification.Condition; import android.service.notification.IConditionListener; import android.service.notification.IConditionProvider; @@ -80,7 +81,8 @@ interface INotificationManager void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim); void setInterruptionFilter(String pkg, int interruptionFilter); - void setImportanceFromRankerService(in INotificationListener token, String key, int importance, CharSequence explanation); + void applyAdjustmentFromRankerService(in INotificationListener token, in Adjustment adjustment); + void applyAdjustmentsFromRankerService(in INotificationListener token, in List<Adjustment> adjustments); ComponentName getEffectsSuppressor(); boolean matchesCallFilter(in Bundle extras); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index faefc9de8f04..4bf1aa38fc13 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -21,6 +21,7 @@ import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SystemApi; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -496,6 +497,15 @@ public class Notification implements Parcelable */ public static final int FLAG_GROUP_SUMMARY = 0x00000200; + /** + * Bit to be bitswise-ored into the {@link #flags} field that should be + * set if this notification is the group summary for an auto-group of notifications. + * + * @hide + */ + @SystemApi + public static final int FLAG_AUTOGROUP_SUMMARY = 0x00000400; + public int flags; /** @hide */ @@ -1945,13 +1955,9 @@ public class Notification implements Parcelable * @hide */ public static void addFieldsFromContext(Context context, Notification notification) { - if (notification.extras.getParcelable(EXTRA_BUILDER_APPLICATION_INFO) == null) { - notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, - context.getApplicationInfo()); - } - if (!notification.extras.containsKey(EXTRA_ORIGINATING_USERID)) { - notification.extras.putInt(EXTRA_ORIGINATING_USERID, context.getUserId()); - } + notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, + context.getApplicationInfo()); + notification.extras.putInt(EXTRA_ORIGINATING_USERID, context.getUserId()); } @Override @@ -3020,12 +3026,13 @@ public class Notification implements Parcelable /** * @hide */ - public void setFlag(int mask, boolean value) { + public Builder setFlag(int mask, boolean value) { if (value) { mN.flags |= mask; } else { mN.flags &= ~mask; } + return this; } /** diff --git a/core/java/android/service/notification/Adjustment.aidl b/core/java/android/service/notification/Adjustment.aidl new file mode 100644 index 000000000000..8bd814a4ca9e --- /dev/null +++ b/core/java/android/service/notification/Adjustment.aidl @@ -0,0 +1,19 @@ +/* + * 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 android.service.notification; + +parcelable Adjustment;
\ No newline at end of file diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java new file mode 100644 index 000000000000..2e4f48d906d8 --- /dev/null +++ b/core/java/android/service/notification/Adjustment.java @@ -0,0 +1,150 @@ +/* + * 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 android.service.notification; + +import android.annotation.SystemApi; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Ranking updates from the Ranker. + * + * @hide + */ +@SystemApi +public final class Adjustment implements Parcelable { + private final String mPackage; + private final String mKey; + private final int mImportance; + private final CharSequence mExplanation; + private final Uri mReference; + private final Bundle mSignals; + + public static final String GROUP_KEY_OVERRIDE_KEY = "group_key_override"; + public static final String NEEDS_AUTOGROUPING_KEY = "autogroup_needed"; + + /** + * Create a notification adjustment. + * + * @param pkg The package of the notification. + * @param key The notification key. + * @param importance The recommended importance of the notification. + * @param signals A bundle of signals that should inform notification grouping and ordering. + * @param explanation A human-readable justification for the adjustment. + * @param reference A reference to an external object that augments the + * explanation, such as a + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, + * or null. + */ + public Adjustment(String pkg, String key, int importance, Bundle signals, + CharSequence explanation, Uri reference) { + mPackage = pkg; + mKey = key; + mImportance = importance; + mSignals = signals; + mExplanation = explanation; + mReference = reference; + } + + protected Adjustment(Parcel in) { + if (in.readInt() == 1) { + mPackage = in.readString(); + } else { + mPackage = null; + } + if (in.readInt() == 1) { + mKey = in.readString(); + } else { + mKey = null; + } + mImportance = in.readInt(); + if (in.readInt() == 1) { + mExplanation = in.readCharSequence(); + } else { + mExplanation = null; + } + mReference = in.readParcelable(Uri.class.getClassLoader()); + mSignals = in.readBundle(); + } + + public static final Creator<Adjustment> CREATOR = new Creator<Adjustment>() { + @Override + public Adjustment createFromParcel(Parcel in) { + return new Adjustment(in); + } + + @Override + public Adjustment[] newArray(int size) { + return new Adjustment[size]; + } + }; + + public String getPackage() { + return mPackage; + } + + public String getKey() { + return mKey; + } + + public int getImportance() { + return mImportance; + } + + public CharSequence getExplanation() { + return mExplanation; + } + + public Uri getReference() { + return mReference; + } + + public Bundle getSignals() { + return mSignals; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mPackage != null) { + dest.writeInt(1); + dest.writeString(mPackage); + } else { + dest.writeInt(0); + } + if (mKey != null) { + dest.writeInt(1); + dest.writeString(mKey); + } else { + dest.writeInt(0); + } + dest.writeInt(mImportance); + if (mExplanation != null) { + dest.writeInt(1); + dest.writeCharSequence(mExplanation); + } else { + dest.writeInt(0); + } + dest.writeParcelable(mReference, flags); + dest.writeBundle(mSignals); + } +} diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index 7325aef5130c..a3f52248848d 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -1052,6 +1052,8 @@ public abstract class NotificationListenerService extends Service { private int mSuppressedVisualEffects; private @Importance int mImportance; private CharSequence mImportanceExplanation; + // System specified group key. + private String mOverrideGroupKey; public Ranking() {} @@ -1130,9 +1132,17 @@ public abstract class NotificationListenerService extends Service { return mImportanceExplanation; } + /** + * If the system has overriden the group key, then this will be non-null, and this + * key should be used to bundle notifications. + */ + public String getOverrideGroupKey() { + return mOverrideGroupKey; + } + private void populate(String key, int rank, boolean matchesInterruptionFilter, int visibilityOverride, int suppressedVisualEffects, int importance, - CharSequence explanation) { + CharSequence explanation, String overrideGroupKey) { mKey = key; mRank = rank; mIsAmbient = importance < IMPORTANCE_LOW; @@ -1141,6 +1151,7 @@ public abstract class NotificationListenerService extends Service { mSuppressedVisualEffects = suppressedVisualEffects; mImportance = importance; mImportanceExplanation = explanation; + mOverrideGroupKey = overrideGroupKey; } /** @@ -1184,6 +1195,7 @@ public abstract class NotificationListenerService extends Service { private ArrayMap<String, Integer> mSuppressedVisualEffects; private ArrayMap<String, Integer> mImportance; private ArrayMap<String, String> mImportanceExplanation; + private ArrayMap<String, String> mOverrideGroupKeys; private RankingMap(NotificationRankingUpdate rankingUpdate) { mRankingUpdate = rankingUpdate; @@ -1210,7 +1222,7 @@ public abstract class NotificationListenerService extends Service { int rank = getRank(key); outRanking.populate(key, rank, !isIntercepted(key), getVisibilityOverride(key), getSuppressedVisualEffects(key), - getImportance(key), getImportanceExplanation(key)); + getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key)); return rank >= 0; } @@ -1281,6 +1293,15 @@ public abstract class NotificationListenerService extends Service { return mImportanceExplanation.get(key); } + private String getOverrideGroupKey(String key) { + synchronized (this) { + if (mOverrideGroupKeys == null) { + buildOverrideGroupKeys(); + } + } + return mOverrideGroupKeys.get(key); + } + // Locked by 'this' private void buildRanksLocked() { String[] orderedKeys = mRankingUpdate.getOrderedKeys(); @@ -1335,6 +1356,15 @@ public abstract class NotificationListenerService extends Service { } } + // Locked by 'this' + private void buildOverrideGroupKeys() { + Bundle overrideGroupKeys = mRankingUpdate.getOverrideGroupKeys(); + mOverrideGroupKeys = new ArrayMap<>(overrideGroupKeys.size()); + for (String key: overrideGroupKeys.keySet()) { + mOverrideGroupKeys.put(key, overrideGroupKeys.getString(key)); + } + } + // ----------- Parcelable @Override diff --git a/core/java/android/service/notification/NotificationRankerService.java b/core/java/android/service/notification/NotificationRankerService.java index 47fdac6bf4c2..ee5361aa9fe6 100644 --- a/core/java/android/service/notification/NotificationRankerService.java +++ b/core/java/android/service/notification/NotificationRankerService.java @@ -22,14 +22,19 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; import android.os.RemoteException; import android.util.Log; import com.android.internal.os.SomeArgs; +import java.util.List; + /** * A service that helps the user manage notifications. This class is only used to * extend the framework service and may not be implemented by non-framework components. @@ -91,27 +96,8 @@ public abstract class NotificationRankerService extends NotificationListenerServ /** Notification was canceled by the owning managed profile being turned off. */ public static final int REASON_PROFILE_TURNED_OFF = 15; - public class Adjustment { - int mImportance; - CharSequence mExplanation; - Uri mReference; - - /** - * Create a notification importance adjustment. - * - * @param importance The final importance of the notification. - * @param explanation A human-readable justification for the adjustment. - * @param reference A reference to an external object that augments the - * explanation, such as a - * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, - * or null. - */ - public Adjustment(int importance, CharSequence explanation, Uri reference) { - mImportance = importance; - mExplanation = explanation; - mReference = reference; - } - } + /** Autobundled summary notification was canceled because its group was unbundled */ + public static final int REASON_UNAUTOBUNDLED = 16; private Handler mHandler; @@ -200,18 +186,32 @@ public abstract class NotificationRankerService extends NotificationListenerServ } /** - * Change the importance of an existing notification. N.B. this won’t cause + * Updates a notification. N.B. this won’t cause * an existing notification to alert, but might allow a future update to * this notification to alert. * - * @param key the notification key - * @param adjustment the new importance with an explanation + * @param adjustment the adjustment with an explanation + */ + public final void adjustNotification(Adjustment adjustment) { + if (!isBound()) return; + try { + getNotificationInterface().applyAdjustmentFromRankerService(mWrapper, adjustment); + } catch (android.os.RemoteException ex) { + Log.v(TAG, "Unable to contact notification manager", ex); + } + } + + /** + * Updates existing notifications. Re-ranking won't occur until all adjustments are applied. + * N.B. this won’t cause an existing notification to alert, but might allow a future update to + * these notifications to alert. + * + * @param adjustments a list of adjustments with explanations */ - public final void adjustImportance(String key, Adjustment adjustment) { + public final void adjustNotifications(List<Adjustment> adjustments) { if (!isBound()) return; try { - getNotificationInterface().setImportanceFromRankerService(mWrapper, key, - adjustment.mImportance, adjustment.mExplanation); + getNotificationInterface().applyAdjustmentsFromRankerService(mWrapper, adjustments); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); } @@ -299,7 +299,7 @@ public abstract class NotificationRankerService extends NotificationListenerServ args.recycle(); Adjustment adjustment = onNotificationEnqueued(sbn, importance, user); if (adjustment != null) { - adjustImportance(sbn.getKey(), adjustment); + adjustNotification(adjustment); } } break; diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index 79f6fc4da1b9..788b5c0da18a 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -30,16 +30,18 @@ public class NotificationRankingUpdate implements Parcelable { private final Bundle mSuppressedVisualEffects; private final int[] mImportance; private final Bundle mImportanceExplanation; + private final Bundle mOverrideGroupKeys; public NotificationRankingUpdate(String[] keys, String[] interceptedKeys, Bundle visibilityOverrides, Bundle suppressedVisualEffects, - int[] importance, Bundle explanation) { + int[] importance, Bundle explanation, Bundle overrideGroupKeys) { mKeys = keys; mInterceptedKeys = interceptedKeys; mVisibilityOverrides = visibilityOverrides; mSuppressedVisualEffects = suppressedVisualEffects; mImportance = importance; mImportanceExplanation = explanation; + mOverrideGroupKeys = overrideGroupKeys; } public NotificationRankingUpdate(Parcel in) { @@ -50,6 +52,7 @@ public class NotificationRankingUpdate implements Parcelable { mImportance = new int[mKeys.length]; in.readIntArray(mImportance); mImportanceExplanation = in.readBundle(); + mOverrideGroupKeys = in.readBundle(); } @Override @@ -65,6 +68,7 @@ public class NotificationRankingUpdate implements Parcelable { out.writeBundle(mSuppressedVisualEffects); out.writeIntArray(mImportance); out.writeBundle(mImportanceExplanation); + out.writeBundle(mOverrideGroupKeys); } public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR @@ -101,4 +105,8 @@ public class NotificationRankingUpdate implements Parcelable { public Bundle getImportanceExplanation() { return mImportanceExplanation; } + + public Bundle getOverrideGroupKeys() { + return mOverrideGroupKeys; + } } diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index 198e43dae4e9..0221b66a8916 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -33,7 +33,8 @@ public class StatusBarNotification implements Parcelable { private final int id; private final String tag; private final String key; - private final String groupKey; + private String groupKey; + private String overrideGroupKey; private final int uid; private final String opPkg; @@ -51,6 +52,27 @@ public class StatusBarNotification implements Parcelable { System.currentTimeMillis()); } + /** @hide */ + public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, + int initialPid, Notification notification, UserHandle user, String overrideGroupKey, + long postTime) { + if (pkg == null) throw new NullPointerException(); + if (notification == null) throw new NullPointerException(); + + this.pkg = pkg; + this.opPkg = opPkg; + this.id = id; + this.tag = tag; + this.uid = uid; + this.initialPid = initialPid; + this.notification = notification; + this.user = user; + this.postTime = postTime; + this.overrideGroupKey = overrideGroupKey; + this.key = key(); + this.groupKey = groupKey(); + } + public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user, long postTime) { @@ -84,15 +106,27 @@ public class StatusBarNotification implements Parcelable { this.notification = new Notification(in); this.user = UserHandle.readFromParcel(in); this.postTime = in.readLong(); + if (in.readInt() != 0) { + this.overrideGroupKey = in.readString(); + } else { + this.overrideGroupKey = null; + } this.key = key(); this.groupKey = groupKey(); } private String key() { - return user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid; + String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid; + if (overrideGroupKey != null && getNotification().isGroupSummary()) { + sbnKey = sbnKey + "|" + overrideGroupKey; + } + return sbnKey; } private String groupKey() { + if (overrideGroupKey != null) { + return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey; + } final String group = getNotification().getGroup(); final String sortKey = getNotification().getSortKey(); if (group == null && sortKey == null) { @@ -105,6 +139,17 @@ public class StatusBarNotification implements Parcelable { : "g:" + group); } + /** + * Returns true if this notification is part of a group. + */ + public boolean isGroup() { + if (overrideGroupKey != null || getNotification().getGroup() != null + || getNotification().getSortKey() != null) { + return true; + } + return false; + } + public void writeToParcel(Parcel out, int flags) { out.writeString(this.pkg); out.writeString(this.opPkg); @@ -121,6 +166,12 @@ public class StatusBarNotification implements Parcelable { user.writeToParcel(out, flags); out.writeLong(this.postTime); + if (this.overrideGroupKey != null) { + out.writeInt(1); + out.writeString(this.overrideGroupKey); + } else { + out.writeInt(0); + } } public int describeContents() { @@ -149,22 +200,22 @@ public class StatusBarNotification implements Parcelable { this.notification.cloneInto(no, false); // light copy return new StatusBarNotification(this.pkg, this.opPkg, this.id, this.tag, this.uid, this.initialPid, - 0, no, this.user, this.postTime); + no, this.user, this.overrideGroupKey, this.postTime); } @Override public StatusBarNotification clone() { return new StatusBarNotification(this.pkg, this.opPkg, this.id, this.tag, this.uid, this.initialPid, - 0, this.notification.clone(), this.user, this.postTime); + this.notification.clone(), this.user, this.overrideGroupKey, this.postTime); } @Override public String toString() { return String.format( - "StatusBarNotification(pkg=%s user=%s id=%d tag=%s score=%d key=%s: %s)", + "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)", this.pkg, this.user, this.id, this.tag, - 0, this.key, this.notification); + this.key, this.notification); } /** Convenience method to check the notification's flags for @@ -258,6 +309,21 @@ public class StatusBarNotification implements Parcelable { } /** + * Sets the override group key. + */ + public void setOverrideGroupKey(String overrideGroupKey) { + this.overrideGroupKey = overrideGroupKey; + groupKey = groupKey(); + } + + /** + * Returns the override group key. + */ + public String getOverrideGroupKey() { + return overrideGroupKey; + } + + /** * @hide */ public Context getPackageContext(Context context) { diff --git a/packages/ExtServices/res/values/strings.xml b/packages/ExtServices/res/values/strings.xml index 076340341d1f..b77ff1055cb1 100644 --- a/packages/ExtServices/res/values/strings.xml +++ b/packages/ExtServices/res/values/strings.xml @@ -17,4 +17,5 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="app_name">Android Services Library</string> <string name="notification_ranker">Android Notification Ranking Service</string> + <string name="notification_ranker_autobundle_explanation">Auto-grouping updated by Ranking Service</string> </resources> diff --git a/packages/ExtServices/src/android/ext/services/notification/Ranker.java b/packages/ExtServices/src/android/ext/services/notification/Ranker.java index 0b2b1a447b91..3ef2aea43a0d 100644 --- a/packages/ExtServices/src/android/ext/services/notification/Ranker.java +++ b/packages/ExtServices/src/android/ext/services/notification/Ranker.java @@ -16,16 +16,36 @@ package android.ext.services.notification; +import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED; + +import android.os.Bundle; +import android.service.notification.Adjustment; import android.service.notification.NotificationRankerService; 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; + +import android.ext.services.R; /** * 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 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 package : notification keys. Only contains notifications that are not bundled + // by the app (aka no group or sort key). + Map<String, LinkedHashSet<String>> mUnbundledNotifications; @Override public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance, @@ -37,10 +57,146 @@ public final class Ranker extends NotificationRankerService { @Override public void onNotificationPosted(StatusBarNotification sbn) { if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey()); + try { + List<String> notificationsToBundle = new ArrayList<>(); + if (!sbn.isGroup()) { + // 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) { + LinkedHashSet<String> notificationsForPackage + = mUnbundledNotifications.get(sbn.getPackageName()); + if (notificationsForPackage == null) { + notificationsForPackage = new LinkedHashSet<>(); + } + if (notificationsForPackage.contains(sbn.getKey())) { + return; + } + notificationsForPackage.add(sbn.getKey()); + mUnbundledNotifications.put(sbn.getPackageName(), notificationsForPackage); + + if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) { + // Autobundle all but the most recently posted (not updated) notification. + int count = 0; + for (String key : notificationsForPackage) { + if (count < notificationsForPackage.size() - 1) { + notificationsToBundle.add(key); + } + count++; + } + } + } + if (notificationsToBundle.size() > 0) { + adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0), + true); + adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true); + } + } else { + // Grouped, but not by us. Send updates to unautobundle, if we bundled it. + maybeUnbundle(sbn, false); + } + } catch (Exception e) { + Slog.e(TAG, "Failure processing new notification", e); + } + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn) { + try { + maybeUnbundle(sbn, true); + } 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) { + List<String> notificationsToUnAutobundle = new ArrayList<>(); + boolean removeSummary = false; + synchronized (mUnbundledNotifications) { + LinkedHashSet<String> notificationsForPackage + = mUnbundledNotifications.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); + } + adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false); + } } @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) { + 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); + 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) { + List<Adjustment> adjustments = new ArrayList<>(); + for (String key : keys) { + adjustments.add(createBundlingAdjustment(packageName, key, bundle)); + 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) { + 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); + } + }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java index c9fe2bded106..6570221bc26a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java @@ -34,6 +34,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Map; import java.util.Objects; /** @@ -257,16 +258,21 @@ public class NotificationData { } public void add(Entry entry, RankingMap ranking) { - mEntries.put(entry.notification.getKey(), entry); - updateRankingAndSort(ranking); + synchronized (mEntries) { + mEntries.put(entry.notification.getKey(), entry); + } mGroupManager.onEntryAdded(entry); + updateRankingAndSort(ranking); } public Entry remove(String key, RankingMap ranking) { - Entry removed = mEntries.remove(key); + Entry removed = null; + synchronized (mEntries) { + removed = mEntries.remove(key); + } if (removed == null) return null; - updateRankingAndSort(ranking); mGroupManager.onEntryRemoved(removed); + updateRankingAndSort(ranking); return removed; } @@ -316,9 +322,30 @@ public class NotificationData { return Ranking.IMPORTANCE_UNSPECIFIED; } + public String getOverrideGroupKey(String key) { + if (mRankingMap != null) { + mRankingMap.getRanking(key, mTmpRanking); + return mTmpRanking.getOverrideGroupKey(); + } + return null; + } + private void updateRankingAndSort(RankingMap ranking) { if (ranking != null) { mRankingMap = ranking; + synchronized (mEntries) { + final int N = mEntries.size(); + for (int i = 0; i < N; i++) { + Entry entry = mEntries.valueAt(i); + final StatusBarNotification oldSbn = entry.notification.clone(); + final String overrideGroupKey = getOverrideGroupKey(entry.key); + if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) { + entry.notification.setOverrideGroupKey(overrideGroupKey); + mGroupManager.onEntryUpdated(entry, oldSbn); + } + //mGroupManager.onEntryBundlingUpdated(entry, getOverrideGroupKey(entry.key)); + } + } } filterAndSort(); } @@ -328,16 +355,18 @@ public class NotificationData { public void filterAndSort() { mSortedAndFiltered.clear(); - final int N = mEntries.size(); - for (int i = 0; i < N; i++) { - Entry entry = mEntries.valueAt(i); - StatusBarNotification sbn = entry.notification; + synchronized (mEntries) { + final int N = mEntries.size(); + for (int i = 0; i < N; i++) { + Entry entry = mEntries.valueAt(i); + StatusBarNotification sbn = entry.notification; - if (shouldFilterOut(sbn)) { - continue; - } + if (shouldFilterOut(sbn)) { + continue; + } - mSortedAndFiltered.add(entry); + mSortedAndFiltered.add(entry); + } } Collections.sort(mSortedAndFiltered, mRankingComparator); @@ -398,16 +427,17 @@ public class NotificationData { NotificationData.Entry e = mSortedAndFiltered.get(active); dumpEntry(pw, indent, active, e); } - - int M = mEntries.size(); - pw.print(indent); - pw.println("inactive notifications: " + (M - active)); - int inactiveCount = 0; - for (int i = 0; i < M; i++) { - Entry entry = mEntries.valueAt(i); - if (!mSortedAndFiltered.contains(entry)) { - dumpEntry(pw, indent, inactiveCount, entry); - inactiveCount++; + synchronized (mEntries) { + int M = mEntries.size(); + pw.print(indent); + pw.println("inactive notifications: " + (M - active)); + int inactiveCount = 0; + for (int i = 0; i < M; i++) { + Entry entry = mEntries.valueAt(i); + if (!mSortedAndFiltered.contains(entry)) { + dumpEntry(pw, indent, inactiveCount, entry); + inactiveCount++; + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java index f7a6b271bcc6..a27ec28b7a07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationGroupManager.java @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import java.util.HashMap; import java.util.HashSet; +import java.util.Objects; /** * A class to handle notifications and their corresponding groups. @@ -121,6 +122,15 @@ public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChanged } } + public void onEntryBundlingUpdated(final NotificationData.Entry updated, + final String overrideGroupKey) { + final StatusBarNotification oldSbn = updated.notification.clone(); + if (!Objects.equals(oldSbn.getOverrideGroupKey(), overrideGroupKey)) { + updated.notification.setOverrideGroupKey(overrideGroupKey); + onEntryUpdated(updated, oldSbn); + } + } + private void updateSuppression(NotificationGroup group) { if (group == null) { return; @@ -129,7 +139,7 @@ public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChanged group.suppressed = group.summary != null && !group.expanded && (group.children.size() == 1 || (group.children.size() == 0 - && !group.summary.notification.getNotification().isGroupChild() + && group.summary.notification.getNotification().isGroupSummary() && hasIsolatedChildren(group))); if (prevSuppressed != group.suppressed) { mListener.onGroupsChanged(); @@ -173,7 +183,7 @@ public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChanged public boolean isOnlyChildInSuppressedGroup(StatusBarNotification sbn) { return isGroupSuppressed(sbn.getGroupKey()) - && sbn.getNotification().isGroupChild() + && !sbn.getNotification().isGroupSummary() && getTotalNumberOfChildren(sbn) == 1; } @@ -278,11 +288,12 @@ public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChanged } return sbn.getNotification().isGroupSummary(); } + private boolean isGroupChild(StatusBarNotification sbn) { if (isIsolated(sbn)) { return false; } - return sbn.getNotification().isGroupChild(); + return sbn.isGroup() && !sbn.getNotification().isGroupSummary(); } private String getGroupKey(StatusBarNotification sbn) { @@ -335,7 +346,7 @@ public class NotificationGroupManager implements HeadsUpManager.OnHeadsUpChanged private boolean shouldIsolate(StatusBarNotification sbn) { NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey()); - return sbn.getNotification().isGroupChild() + return (sbn.isGroup() && !sbn.getNotification().isGroupSummary()) && (sbn.getNotification().fullScreenIntent != null || notificationGroup == null || !notificationGroup.expanded diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 99c41eae22ae..c855276c98e0 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -30,6 +30,7 @@ import static android.service.notification.NotificationRankerService.REASON_PACK import static android.service.notification.NotificationRankerService.REASON_PACKAGE_CHANGED; import static android.service.notification.NotificationRankerService.REASON_PACKAGE_SUSPENDED; import static android.service.notification.NotificationRankerService.REASON_PROFILE_TURNED_OFF; +import static android.service.notification.NotificationRankerService.REASON_UNAUTOBUNDLED; import static android.service.notification.NotificationRankerService.REASON_USER_STOPPED; import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS; import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF; @@ -39,8 +40,6 @@ import static android.service.notification.NotificationListenerService.TRIM_LIGH import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_DEFAULT; import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_NONE; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; -import static org.xmlpull.v1.XmlPullParser.END_TAG; -import static org.xmlpull.v1.XmlPullParser.START_TAG; import android.Manifest; import android.annotation.Nullable; @@ -97,6 +96,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.os.Vibrator; import android.provider.Settings; +import android.service.notification.Adjustment; import android.service.notification.Condition; import android.service.notification.IConditionProvider; import android.service.notification.INotificationListener; @@ -136,7 +136,6 @@ import com.android.server.notification.ManagedServices.UserProfiles; import libcore.io.IoUtils; -import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; @@ -158,7 +157,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -263,6 +261,7 @@ public class NotificationManagerService extends SystemService { new ArrayList<NotificationRecord>(); final ArrayMap<String, NotificationRecord> mNotificationsByKey = new ArrayMap<String, NotificationRecord>(); + final ArrayMap<String, String> mAutobundledSummaries = new ArrayMap<>(); final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>(); final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>(); final PolicyAccess mPolicyAccess = new PolicyAccess(); @@ -283,11 +282,6 @@ public class NotificationManagerService extends SystemService { private static final String TAG_NOTIFICATION_POLICY = "notification-policy"; private static final String ATTR_VERSION = "version"; - // Obsolete: converted if present, but not resaved to disk. - private static final String TAG_BLOCKED_PKGS = "blocked-packages"; - private static final String TAG_PACKAGE = "package"; - private static final String ATTR_NAME = "name"; - private RankingHelper mRankingHelper; private final UserProfiles mUserProfiles = new UserProfiles(); @@ -1259,10 +1253,13 @@ public class NotificationManagerService extends SystemService { checkCallerIsSystemOrSameApp(pkg); userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, true, false, "cancelNotificationWithTag", pkg); - // Don't allow client applications to cancel foreground service notis. + // Don't allow client applications to cancel foreground service notis or autobundled + // summaries. cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), pkg, tag, id, 0, - Binder.getCallingUid() == Process.SYSTEM_UID - ? 0 : Notification.FLAG_FOREGROUND_SERVICE, false, userId, + (Binder.getCallingUid() == Process.SYSTEM_UID + ? 0 : Notification.FLAG_FOREGROUND_SERVICE) + | (Binder.getCallingUid() == Process.SYSTEM_UID + ? 0 : Notification.FLAG_AUTOGROUP_SUMMARY), false, userId, REASON_APP_CANCEL, null); } @@ -1404,7 +1401,9 @@ public class NotificationManagerService extends SystemService { final int N = mNotificationList.size(); for (int i = 0; i < N; i++) { final StatusBarNotification sbn = mNotificationList.get(i).sbn; - if (sbn.getPackageName().equals(pkg) && sbn.getUserId() == userId) { + if (sbn.getPackageName().equals(pkg) && sbn.getUserId() == userId + && (sbn.getNotification().flags + & Notification.FLAG_AUTOGROUP_SUMMARY) != 0) { // We could pass back a cloneLight() but clients might get confused and // try to send this thing back to notify() again, which would not work // very well. @@ -1519,7 +1518,8 @@ public class NotificationManagerService extends SystemService { checkCallerIsSystemOrSameApp(component.getPackageName()); long identity = Binder.clearCallingIdentity(); try { - ManagedServices manager = mRankerServices.isComponentEnabledForCurrentProfiles(component) + ManagedServices manager = + mRankerServices.isComponentEnabledForCurrentProfiles(component) ? mRankerServices : mListeners; manager.setComponentState(component, true); @@ -2035,25 +2035,123 @@ public class NotificationManagerService extends SystemService { } @Override - public void setImportanceFromRankerService(INotificationListener token, String key, - int importance, CharSequence explanation) throws RemoteException { - if (importance == IMPORTANCE_NONE) { - throw new IllegalArgumentException("blocking not allowed: key=" + key); + public void applyAdjustmentFromRankerService(INotificationListener token, + Adjustment adjustment) throws RemoteException { + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mNotificationList) { + mRankerServices.checkServiceTokenLocked(token); + applyAdjustmentLocked(adjustment); + } + maybeAddAutobundleSummary(adjustment); + mRankingHandler.requestSort(); + } finally { + Binder.restoreCallingIdentity(identity); } + } + + @Override + public void applyAdjustmentsFromRankerService(INotificationListener token, + List<Adjustment> adjustments) throws RemoteException { + final long identity = Binder.clearCallingIdentity(); try { synchronized (mNotificationList) { mRankerServices.checkServiceTokenLocked(token); - NotificationRecord n = mNotificationsByKey.get(key); - n.setImportance(importance, explanation); - mRankingHandler.requestSort(); + for (Adjustment adjustment : adjustments) { + applyAdjustmentLocked(adjustment); + } } + for (Adjustment adjustment : adjustments) { + maybeAddAutobundleSummary(adjustment); + } + mRankingHandler.requestSort(); } finally { Binder.restoreCallingIdentity(identity); } } }; + private void applyAdjustmentLocked(Adjustment adjustment) { + maybeClearAutobundleSummaryLocked(adjustment); + NotificationRecord n = mNotificationsByKey.get(adjustment.getKey()); + if (n == null) { + return; + } + if (adjustment.getImportance() != IMPORTANCE_NONE) { + n.setImportance(adjustment.getImportance(), adjustment.getExplanation()); + } + if (adjustment.getSignals() != null) { + Bundle.setDefusable(adjustment.getSignals(), true); + n.sbn.setOverrideGroupKey(adjustment.getSignals().getString( + Adjustment.GROUP_KEY_OVERRIDE_KEY, null)); + } + } + + // Clears the 'fake' auto-bunding summary. + private void maybeClearAutobundleSummaryLocked(Adjustment adjustment) { + if (adjustment.getSignals() != null + && adjustment.getSignals().containsKey(Adjustment.NEEDS_AUTOGROUPING_KEY) + && !adjustment.getSignals().getBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false)) { + if (mAutobundledSummaries.containsKey(adjustment.getPackage())) { + // Clear summary. + final NotificationRecord removed = mNotificationsByKey.get( + mAutobundledSummaries.remove(adjustment.getPackage())); + 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 + && 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) { + if (!mAutobundledSummaries.containsKey(adjustment.getPackage()) + && newAutoBundleKey != null) { + // Add summary + final StatusBarNotification adjustedSbn + = mNotificationsByKey.get(adjustment.getKey()).sbn; + + 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) + .build(); + summaryNotification.extras.putAll(extras); + 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); + mAutobundledSummaries.put(adjustment.getPackage(), summarySbn.getKey()); + userId = adjustedSbn.getUser().getIdentifier(); + } + } + if (summaryRecord != null) { + mHandler.post(new EnqueueNotificationRunnable(userId, summaryRecord)); + } + } + } + private String disableNotificationEffects(NotificationRecord record) { if (mDisableNotificationEffects) { return "booleanState"; @@ -2253,6 +2351,17 @@ public class NotificationManagerService extends SystemService { callingUid, incomingUserId, true, false, "enqueueNotification", pkg); final UserHandle user = new UserHandle(userId); + // Fix the notification as best we can. + try { + Notification.addFieldsFromContext(getContext().createApplicationContext( + getContext().getPackageManager().getApplicationInfoAsUser( + pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId), + Context.CONTEXT_RESTRICTED), notification); + } catch (NameNotFoundException e) { + Slog.e(TAG, "Cannot create a context for sending app", e); + return; + } + // Limit the number of notifications that any given package except the android // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { @@ -2492,7 +2601,7 @@ public class NotificationManagerService extends SystemService { StatusBarNotification sbn = r.sbn; String group = sbn.getGroupKey(); boolean isSummary = sbn.getNotification().isGroupSummary(); - boolean isChild = sbn.getNotification().isGroupChild(); + boolean isChild = !isSummary && sbn.isGroup(); NotificationRecord summary = mSummaryByGroupKey.get(group); if (isChild && summary != null) { @@ -2857,11 +2966,13 @@ public class NotificationManagerService extends SystemService { synchronized (mNotificationList) { final int N = mNotificationList.size(); ArrayList<String> orderBefore = new ArrayList<String>(N); + ArrayList<String> groupOverrideBefore = new ArrayList<>(N); int[] visibilities = new int[N]; - int [] importances = new int[N]; + int[] importances = new int[N]; for (int i = 0; i < N; i++) { final NotificationRecord r = mNotificationList.get(i); orderBefore.add(r.getKey()); + groupOverrideBefore.add(r.sbn.getGroupKey()); visibilities[i] = r.getPackageVisibilityOverride(); importances[i] = r.getImportance(); mRankingHelper.extractSignals(r); @@ -2871,7 +2982,8 @@ public class NotificationManagerService extends SystemService { final NotificationRecord r = mNotificationList.get(i); if (!orderBefore.get(i).equals(r.getKey()) || visibilities[i] != r.getPackageVisibilityOverride() - || importances[i] != r.getImportance()) { + || importances[i] != r.getImportance() + || !groupOverrideBefore.get(i).equals(r.sbn.getGroupKey())) { scheduleSendRankingUpdate(); return; } @@ -3070,6 +3182,7 @@ public class NotificationManagerService extends SystemService { mLights.remove(canceledKey); // Record usage stats + // TODO: add unbundling stats? switch (reason) { case REASON_DELEGATE_CANCEL: case REASON_DELEGATE_CANCEL_ALL: @@ -3089,6 +3202,9 @@ public class NotificationManagerService extends SystemService { if (groupSummary != null && groupSummary.getKey().equals(r.getKey())) { mSummaryByGroupKey.remove(groupKey); } + if (r.sbn.getKey().equals(mAutobundledSummaries.get(r.sbn.getPackageName()))) { + mAutobundledSummaries.remove(r.sbn.getPackageName()); + } // Save it for users of getHistoricalNotifications() mArchive.record(r.sbn); @@ -3287,7 +3403,7 @@ public class NotificationManagerService extends SystemService { for (int i = N - 1; i >= 0; i--) { NotificationRecord childR = mNotificationList.get(i); StatusBarNotification childSbn = childR.sbn; - if (childR.getNotification().isGroupChild() && + if ((childSbn.isGroup() && !childSbn.getNotification().isGroupSummary()) && childR.getGroupKey().equals(r.getGroupKey())) { EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(), childSbn.getTag(), userId, 0, 0, reason, listenerName); @@ -3438,6 +3554,7 @@ public class NotificationManagerService extends SystemService { ArrayList<String> keys = new ArrayList<String>(N); ArrayList<String> interceptedKeys = new ArrayList<String>(N); ArrayList<Integer> importance = new ArrayList<>(N); + Bundle overrideGroupKeys = new Bundle(); Bundle visibilityOverrides = new Bundle(); Bundle suppressedVisualEffects = new Bundle(); Bundle explanation = new Bundle(); @@ -3461,6 +3578,7 @@ public class NotificationManagerService extends SystemService { != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) { visibilityOverrides.putInt(key, record.getPackageVisibilityOverride()); } + overrideGroupKeys.putString(key, record.sbn.getOverrideGroupKey()); } final int M = keys.size(); String[] keysAr = keys.toArray(new String[M]); @@ -3470,7 +3588,7 @@ public class NotificationManagerService extends SystemService { importanceAr[i] = importance.get(i); } return new NotificationRankingUpdate(keysAr, interceptedKeysAr, visibilityOverrides, - suppressedVisualEffects, importanceAr, explanation); + suppressedVisualEffects, importanceAr, explanation, overrideGroupKeys); } private boolean isVisibleToListener(StatusBarNotification sbn, ManagedServiceInfo listener) { diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index fd893fa081a5..a89a4227863a 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -24,18 +24,15 @@ import static android.service.notification.NotificationListenerService.Ranking.I import android.app.Notification; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Icon; import android.media.AudioAttributes; -import android.os.Build; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; -import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.EventLogTags; diff --git a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java index 0da1bb193c98..6c8be3978ccf 100644 --- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java +++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java @@ -86,6 +86,155 @@ public class NotificationTestList extends TestActivity } private Test[] mTests = new Test[] { + new Test("Post a group") { + public void run() + { + Notification n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("Min priority group 1") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_MIN) + .setGroup("group1") + .build(); + mNM.notify(6000, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("low priority group 1") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_LOW) + .setGroup("group1") + .build(); + mNM.notify(6001, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("default priority group 1") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_DEFAULT) + .setGroup("group1") + .build(); + mNM.notify(6002, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("summary group 1") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_MIN) + .setGroup("group1") + .setGroupSummary(true) + .build(); + mNM.notify(6003, n); + } + }, + new Test("Post a group (2) w/o summary") { + public void run() + { + Notification n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("Min priority group 2") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_MIN) + .setGroup("group2") + .build(); + mNM.notify(6100, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("low priority group 2") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_LOW) + .setGroup("group2") + .build(); + mNM.notify(6101, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("default priority group 2") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_DEFAULT) + .setGroup("group2") + .build(); + mNM.notify(6102, n); + } + }, + new Test("Summary for group 2") { + public void run() + { + Notification n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("summary group 2") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_MIN) + .setGroup("group2") + .setGroupSummary(true) + .build(); + mNM.notify(6103, n); + } + }, + new Test("Group up public-secret") { + public void run() + { + Notification n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("public notification") + .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE) + .setPriority(Notification.PRIORITY_DEFAULT) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setGroup("public-secret") + .build(); + mNM.notify("public", 7009, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("private only notification") + .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE) + .setPriority(Notification.PRIORITY_DEFAULT) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setGroup("public-secret") + .build(); + mNM.notify("no public", 7010, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("private version of notification") + .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE) + .setPriority(Notification.PRIORITY_DEFAULT) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setGroup("public-secret") + .setPublicVersion(new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("public notification of private notification") + .setPriority(Notification.PRIORITY_DEFAULT) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .build()) + .build(); + mNM.notify("priv with pub", 7011, n); + n = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("secret notification") + .setDefaults(Notification.DEFAULT_LIGHTS|Notification.DEFAULT_VIBRATE) + .setPriority(Notification.PRIORITY_DEFAULT) + .setVisibility(Notification.VISIBILITY_SECRET) + .setGroup("public-secret") + .build(); + mNM.notify("secret", 7012, n); + + Notification s = new Notification.Builder(NotificationTestList.this) + .setSmallIcon(R.drawable.icon2) + .setContentTitle("summary group public-secret") + .setLights(0xff0000ff, 1, 0) + .setPriority(Notification.PRIORITY_MIN) + .setGroup("public-secret") + .setGroupSummary(true) + .build(); + mNM.notify(7113, s); + } + }, + new Test("Cancel priority autogroup") { + public void run() + { + try { + mNM.cancel(Integer.MAX_VALUE); + } catch (Exception e) { + Toast.makeText(NotificationTestList.this, "cancel failed (yay)", + Toast.LENGTH_LONG).show(); + } + } + }, new Test("Min priority") { public void run() { @@ -95,7 +244,7 @@ public class NotificationTestList extends TestActivity .setLights(0xff0000ff, 1, 0) .setPriority(Notification.PRIORITY_MIN) .build(); - mNM.notify(7000, n); + mNM.notify("min", 7000, n); } }, new Test("Min priority, high pri flag") { @@ -123,7 +272,7 @@ public class NotificationTestList extends TestActivity .setLights(0xff0000ff, 1, 0) .setPriority(Notification.PRIORITY_LOW) .build(); - mNM.notify(7002, n); + mNM.notify("low", 7002, n); } }, new Test("Default priority") { @@ -135,7 +284,7 @@ public class NotificationTestList extends TestActivity .setLights(0xff0000ff, 1, 0) .setPriority(Notification.PRIORITY_DEFAULT) .build(); - mNM.notify(7004, n); + mNM.notify("default", 7004, n); } }, new Test("High priority") { @@ -150,7 +299,7 @@ public class NotificationTestList extends TestActivity getPackageName() + "/raw/ringer")) .setPriority(Notification.PRIORITY_HIGH) .build(); - mNM.notify(7006, n); + mNM.notify("high", 7006, n); } }, new Test("Max priority") { @@ -166,7 +315,7 @@ public class NotificationTestList extends TestActivity .setPriority(Notification.PRIORITY_MAX) .setFullScreenIntent(makeIntent2(), false) .build(); - mNM.notify(7007, n); + mNM.notify("max", 7007, n); } }, new Test("Max priority with delay") { @@ -199,7 +348,7 @@ public class NotificationTestList extends TestActivity .setPriority(Notification.PRIORITY_DEFAULT) .setVisibility(Notification.VISIBILITY_PUBLIC) .build(); - mNM.notify(7009, n); + mNM.notify("public", 7009, n); } }, new Test("private notification, no public") { @@ -212,7 +361,7 @@ public class NotificationTestList extends TestActivity .setPriority(Notification.PRIORITY_DEFAULT) .setVisibility(Notification.VISIBILITY_PRIVATE) .build(); - mNM.notify(7010, n); + mNM.notify("no public", 7010, n); } }, new Test("private notification, has public") { @@ -231,7 +380,7 @@ public class NotificationTestList extends TestActivity .setVisibility(Notification.VISIBILITY_PUBLIC) .build()) .build(); - mNM.notify(7011, n); + mNM.notify("priv with pub", 7011, n); } }, new Test("secret notification") { @@ -244,7 +393,7 @@ public class NotificationTestList extends TestActivity .setPriority(Notification.PRIORITY_DEFAULT) .setVisibility(Notification.VISIBILITY_SECRET) .build(); - mNM.notify(7012, n); + mNM.notify("secret", 7012, n); } }, new Test("Off") { |