Change notification shade ordering.
Certain ongoing and people centric notifications can now
(mostly) trump importance-based ordering.
Bug: 30374279
Test: runtest systemui-notification
Change-Id: Ieab6015174f9595c08057dc408233202035aae3e
diff --git a/api/system-current.txt b/api/system-current.txt
index d8eff57..d3f418c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5141,6 +5141,7 @@
method public java.lang.String getChannel();
method public java.lang.String getGroup();
method public android.graphics.drawable.Icon getLargeIcon();
+ method public static java.lang.Class<? extends android.app.Notification.Style> getNotificationStyleClass(java.lang.String);
method public android.graphics.drawable.Icon getSmallIcon();
method public java.lang.String getSortKey();
method public void writeToParcel(android.os.Parcel, int);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index c1e2072..119b055 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3985,19 +3985,6 @@
return new Builder(builderContext, n);
}
- private static Class<? extends Style> getNotificationStyleClass(String templateClass) {
- Class<? extends Style>[] classes = new Class[] {
- BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
- DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
- MessagingStyle.class };
- for (Class<? extends Style> innerClass : classes) {
- if (templateClass.equals(innerClass.getName())) {
- return innerClass;
- }
- }
- return null;
- }
-
/**
* @deprecated Use {@link #build()} instead.
*/
@@ -4175,6 +4162,23 @@
}
/**
+ * @hide
+ */
+ @SystemApi
+ public static Class<? extends Style> getNotificationStyleClass(String templateClass) {
+ Class<? extends Style>[] classes = new Class[] {
+ BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
+ DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
+ MessagingStyle.class };
+ for (Class<? extends Style> innerClass : classes) {
+ if (templateClass.equals(innerClass.getName())) {
+ return innerClass;
+ }
+ }
+ return null;
+ }
+
+ /**
* An object that can apply a rich notification style to a {@link Notification.Builder}
* object.
*/
diff --git a/services/core/java/com/android/server/notification/NotificationComparator.java b/services/core/java/com/android/server/notification/NotificationComparator.java
index 7dff2c1..1e0035d 100644
--- a/services/core/java/com/android/server/notification/NotificationComparator.java
+++ b/services/core/java/com/android/server/notification/NotificationComparator.java
@@ -15,7 +15,25 @@
*/
package com.android.server.notification;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Slog;
+
import java.util.Comparator;
+import java.util.Objects;
/**
* Sorts notifications individually into attention-relevant order.
@@ -23,8 +41,45 @@
public class NotificationComparator
implements Comparator<NotificationRecord> {
+ private final String DEFAULT_SMS_APP_SETTING = Settings.Secure.SMS_DEFAULT_APPLICATION;
+
+ private final Context mContext;
+ private String mDefaultPhoneApp;
+ private ArrayMap<Integer, String> mDefaultSmsApp = new ArrayMap<>();
+
+ public NotificationComparator(Context context) {
+ mContext = context;
+ mContext.registerReceiver(mPhoneAppBroadcastReceiver,
+ new IntentFilter(TelecomManager.ACTION_DEFAULT_DIALER_CHANGED));
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Secure.getUriFor(DEFAULT_SMS_APP_SETTING), false, mSmsContentObserver);
+ }
+
@Override
public int compare(NotificationRecord left, NotificationRecord right) {
+ // First up: sufficiently important ongoing notifications of certain categories
+ boolean leftImportantOngoing = isImportantOngoing(left);
+ boolean rightImportantOngoing = isImportantOngoing(right);
+
+ if (leftImportantOngoing != rightImportantOngoing) {
+ // by ongoing, ongoing higher than non-ongoing
+ return -1 * Boolean.compare(leftImportantOngoing, rightImportantOngoing);
+ }
+
+ // Next: sufficiently import person to person communication
+ boolean leftPeople = isImportantMessaging(left);
+ boolean rightPeople = isImportantMessaging(right);
+
+ if (leftPeople && rightPeople){
+ // by contact proximity, close to far. if same proximity, check further fields.
+ if (Float.compare(left.getContactAffinity(), right.getContactAffinity()) != 0) {
+ return -1 * Float.compare(left.getContactAffinity(), right.getContactAffinity());
+ }
+ } else if (leftPeople != rightPeople) {
+ // People, messaging higher than non-messaging
+ return -1 * Boolean.compare(leftPeople, rightPeople);
+ }
+
final int leftImportance = left.getImportance();
final int rightImportance = right.getImportance();
if (leftImportance != rightImportance) {
@@ -47,14 +102,112 @@
return -1 * Integer.compare(leftPriority, rightPriority);
}
- final float leftPeople = left.getContactAffinity();
- final float rightPeople = right.getContactAffinity();
- if (leftPeople != rightPeople) {
- // by contact proximity, close to far
- return -1 * Float.compare(leftPeople, rightPeople);
- }
-
// then break ties by time, most recent first
return -1 * Long.compare(left.getRankingTimeMs(), right.getRankingTimeMs());
}
+
+ private boolean isImportantOngoing(NotificationRecord record) {
+ if (!isOngoing(record)) {
+ return false;
+ }
+
+ if (record.getImportance() < NotificationManager.IMPORTANCE_LOW) {
+ return false;
+ }
+
+ // TODO: add whitelist
+
+ return isCall(record) || isMediaNotification(record);
+ }
+
+ protected boolean isImportantMessaging(NotificationRecord record) {
+ if (record.getImportance() < NotificationManager.IMPORTANCE_LOW) {
+ return false;
+ }
+
+ Class<? extends Notification.Style> style = getNotificationStyle(record);
+ if (Notification.MessagingStyle.class.equals(style)) {
+ return true;
+ }
+
+ if (record.getContactAffinity() > ValidateNotificationPeople.NONE) {
+ return true;
+ }
+
+ if (record.getNotification().category == Notification.CATEGORY_MESSAGE
+ && isDefaultMessagingApp(record)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isOngoing(NotificationRecord record) {
+ final int ongoingFlags =
+ Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_ONGOING_EVENT;
+ return (record.getNotification().flags & ongoingFlags) != 0;
+ }
+
+
+ private Class<? extends Notification.Style> getNotificationStyle(NotificationRecord record) {
+ String templateClass =
+ record.getNotification().extras.getString(Notification.EXTRA_TEMPLATE);
+
+ if (!TextUtils.isEmpty(templateClass)) {
+ return Notification.getNotificationStyleClass(templateClass);
+ }
+ return null;
+ }
+
+ private boolean isMediaNotification(NotificationRecord record) {
+ return record.getNotification().extras.getParcelable(
+ Notification.EXTRA_MEDIA_SESSION) != null;
+ }
+
+ private boolean isCall(NotificationRecord record) {
+ return record.getNotification().category == Notification.CATEGORY_CALL
+ && isDefaultPhoneApp(record.sbn.getPackageName());
+ }
+
+ private boolean isDefaultPhoneApp(String pkg) {
+ if (mDefaultPhoneApp == null) {
+ final TelecomManager telecomm =
+ (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+ mDefaultPhoneApp = telecomm != null ? telecomm.getDefaultDialerPackage() : null;
+ }
+ return Objects.equals(pkg, mDefaultPhoneApp);
+ }
+
+ @SuppressWarnings("deprecation")
+ private boolean isDefaultMessagingApp(NotificationRecord record) {
+ final int userId = record.getUserId();
+ if (userId == UserHandle.USER_NULL || userId == UserHandle.USER_ALL) return false;
+ if (mDefaultSmsApp.get(userId) == null) {
+ mDefaultSmsApp.put(userId, Settings.Secure.getStringForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.SMS_DEFAULT_APPLICATION, userId));
+ }
+ return Objects.equals(mDefaultSmsApp.get(userId), record.sbn.getPackageName());
+ }
+
+ private final BroadcastReceiver mPhoneAppBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mDefaultPhoneApp =
+ intent.getStringExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME);
+ }
+ };
+
+ private final ContentObserver mSmsContentObserver = new ContentObserver(
+ new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri, int userId) {
+ if (Settings.Secure.getUriFor(DEFAULT_SMS_APP_SETTING).equals(uri)) {
+ mDefaultSmsApp.put(userId, Settings.Secure.getStringForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.SMS_DEFAULT_APPLICATION, userId));
+
+ }
+ }
+ };
}
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 89101a8..95718de 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -73,7 +73,7 @@
private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
private final NotificationSignalExtractor[] mSignalExtractors;
- private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
+ private final NotificationComparator mPreliminaryComparator;
private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
@@ -90,6 +90,8 @@
mRankingHandler = rankingHandler;
mPm = pm;
+ mPreliminaryComparator = new NotificationComparator(mContext);
+
final int N = extractorNames.length;
mSignalExtractors = new NotificationSignalExtractor[N];
for (int i = 0; i < N; i++) {
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/notification/src/com/android/server/notification/NotificationComparatorTest.java
new file mode 100644
index 0000000..403b65c
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/NotificationComparatorTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.media.session.MediaSession;
+import android.os.Build;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.service.notification.StatusBarNotification;
+import android.telecom.TelecomManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationComparatorTest {
+ @Mock Context mContext;
+ @Mock TelecomManager mTm;
+ @Mock RankingHandler handler;
+ @Mock PackageManager mPm;
+
+ private final String callPkg = "com.android.server.notification";
+ private final int callUid = 10;
+ private String smsPkg;
+ private final int smsUid = 11;
+ private final String pkg2 = "pkg2";
+ private final int uid2 = 1111111;
+
+ private NotificationRecord mRecordMinCall;
+ private NotificationRecord mRecordHighCall;
+ private NotificationRecord mRecordDefaultMedia;
+ private NotificationRecord mRecordEmail;
+ private NotificationRecord mRecordInlineReply;
+ private NotificationRecord mRecordSms;
+ private NotificationRecord mRecordStarredContact;
+ private NotificationRecord mRecordContact;
+ private NotificationRecord mRecordUrgent;
+ private NotificationRecord mRecordCheater;
+
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ int userId = UserHandle.myUserId();
+
+ when(mContext.getResources()).thenReturn(
+ InstrumentationRegistry.getTargetContext().getResources());
+ when(mContext.getContentResolver()).thenReturn(
+ InstrumentationRegistry.getTargetContext().getContentResolver());
+ when(mContext.getPackageManager()).thenReturn(mPm);
+ when(mContext.getSystemService(eq(Context.TELECOM_SERVICE))).thenReturn(mTm);
+ when(mTm.getDefaultDialerPackage()).thenReturn(callPkg);
+ final ApplicationInfo legacy = new ApplicationInfo();
+ legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1;
+ try {
+ when(mPm.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())).thenReturn(legacy);
+ when(mContext.getApplicationInfo()).thenReturn(legacy);
+ } catch (PackageManager.NameNotFoundException e) {
+ // let's hope not
+ }
+
+ smsPkg = Settings.Secure.getString(mContext.getContentResolver(),
+ Settings.Secure.SMS_DEFAULT_APPLICATION);
+
+ Notification n1 = new Notification.Builder(mContext)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
+ .build();
+ mRecordMinCall = new NotificationRecord(mContext, new StatusBarNotification(callPkg,
+ callPkg, getDefaultChannel(), 1, "minCall", callUid, callUid, n1,
+ new UserHandle(userId), "", 2000));
+ mRecordMinCall.setUserImportance(NotificationManager.IMPORTANCE_MIN);
+
+ Notification n2 = new Notification.Builder(mContext)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
+ .build();
+ mRecordHighCall = new NotificationRecord(mContext, new StatusBarNotification(callPkg,
+ callPkg, getDefaultChannel(), 1, "highcall", callUid, callUid, n2,
+ new UserHandle(userId), "", 1999));
+ mRecordHighCall.setUserImportance(NotificationManager.IMPORTANCE_HIGH);
+
+ Notification n3 = new Notification.Builder(mContext)
+ .setStyle(new Notification.MediaStyle()
+ .setMediaSession(new MediaSession.Token(null)))
+ .setFlag(Notification.FLAG_FOREGROUND_SERVICE, true)
+ .build();
+ mRecordDefaultMedia = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "media", uid2, uid2, n3, new UserHandle(userId),
+ "", 1499));
+ mRecordDefaultMedia.setUserImportance(NotificationManager.IMPORTANCE_DEFAULT);
+
+ Notification n4 = new Notification.Builder(mContext)
+ .setStyle(new Notification.MessagingStyle("sender!")).build();
+ mRecordInlineReply = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "inlinereply", uid2, uid2, n4, new UserHandle(userId),
+ "", 1599));
+ mRecordInlineReply.setUserImportance(NotificationManager.IMPORTANCE_HIGH);
+ mRecordInlineReply.setPackagePriority(Notification.PRIORITY_MAX);
+
+ Notification n5 = new Notification.Builder(mContext)
+ .setCategory(Notification.CATEGORY_MESSAGE).build();
+ mRecordSms = new NotificationRecord(mContext, new StatusBarNotification(smsPkg,
+ smsPkg, getDefaultChannel(), 1, "sms", smsUid, smsUid, n5, new UserHandle(userId),
+ "", 1299));
+ mRecordSms.setUserImportance(NotificationManager.IMPORTANCE_DEFAULT);
+
+ Notification n6 = new Notification.Builder(mContext).build();
+ mRecordStarredContact = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "starred", uid2, uid2, n6, new UserHandle(userId),
+ "", 1259));
+ mRecordStarredContact.setContactAffinity(ValidateNotificationPeople.STARRED_CONTACT);
+ mRecordStarredContact.setUserImportance(NotificationManager.IMPORTANCE_DEFAULT);
+
+ Notification n7 = new Notification.Builder(mContext).build();
+ mRecordContact = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "contact", uid2, uid2, n7, new UserHandle(userId),
+ "", 1259));
+ mRecordContact.setContactAffinity(ValidateNotificationPeople.VALID_CONTACT);
+ mRecordContact.setUserImportance(NotificationManager.IMPORTANCE_DEFAULT);
+
+ Notification n8 = new Notification.Builder(mContext).build();
+ mRecordUrgent = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "urgent", uid2, uid2, n8, new UserHandle(userId),
+ "", 1258));
+ mRecordUrgent.setUserImportance(NotificationManager.IMPORTANCE_HIGH);
+
+ Notification n9 = new Notification.Builder(mContext)
+ .setCategory(Notification.CATEGORY_MESSAGE)
+ .setFlag(Notification.FLAG_ONGOING_EVENT
+ |Notification.FLAG_FOREGROUND_SERVICE, true)
+ .build();
+ mRecordCheater = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "cheater", uid2, uid2, n9, new UserHandle(userId),
+ "", 9258));
+ mRecordCheater.setUserImportance(NotificationManager.IMPORTANCE_LOW);
+
+ Notification n10 = new Notification.Builder(mContext)
+ .setStyle(new Notification.InboxStyle().setSummaryText("message!")).build();
+ mRecordEmail = new NotificationRecord(mContext, new StatusBarNotification(pkg2,
+ pkg2, getDefaultChannel(), 1, "email", uid2, uid2, n10, new UserHandle(userId),
+ "", 1599));
+ mRecordEmail.setUserImportance(NotificationManager.IMPORTANCE_HIGH);
+ }
+
+ @Test
+ public void testOrdering() throws Exception {
+ final List<NotificationRecord> expected = new ArrayList<>();
+ expected.add(mRecordHighCall);
+ expected.add(mRecordDefaultMedia);
+ expected.add(mRecordStarredContact);
+ expected.add(mRecordContact);
+ expected.add(mRecordInlineReply);
+ expected.add(mRecordSms);
+ expected.add(mRecordEmail);
+ expected.add(mRecordUrgent);
+ expected.add(mRecordCheater);
+ expected.add(mRecordMinCall);
+
+ List<NotificationRecord> actual = new ArrayList<>();
+ actual.addAll(expected);
+ Collections.shuffle(actual);
+
+ Collections.sort(actual, new NotificationComparator(mContext));
+
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testMessaging() throws Exception {
+ NotificationComparator comp = new NotificationComparator(mContext);
+ assertTrue(comp.isImportantMessaging(mRecordStarredContact));
+ assertTrue(comp.isImportantMessaging(mRecordContact));
+ assertTrue(comp.isImportantMessaging(mRecordInlineReply));
+ assertTrue(comp.isImportantMessaging(mRecordSms));
+ assertFalse(comp.isImportantMessaging(mRecordEmail));
+ assertFalse(comp.isImportantMessaging(mRecordCheater));
+ }
+
+ private NotificationChannel getDefaultChannel() {
+ return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "name",
+ NotificationManager.IMPORTANCE_LOW);
+ }
+}