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);
+    }
+}