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
This commit is contained in:
Julia Reynolds
2016-12-13 13:39:52 -05:00
parent 9ed3a080e8
commit 4a02afbf04
5 changed files with 403 additions and 21 deletions

View File

@@ -5141,6 +5141,7 @@ package android.app {
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);

View File

@@ -3985,19 +3985,6 @@ public class Notification implements Parcelable
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.
*/
@@ -4174,6 +4161,23 @@ public class Notification implements Parcelable
return when != 0 && extras.getBoolean(EXTRA_SHOW_CHRONOMETER);
}
/**
* @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.

View File

@@ -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 @@ import java.util.Comparator;
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 @@ public class NotificationComparator
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));
}
}
};
}

View File

@@ -73,7 +73,7 @@ public class RankingHelper implements RankingConfig {
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 @@ public class RankingHelper implements RankingConfig {
mRankingHandler = rankingHandler;
mPm = pm;
mPreliminaryComparator = new NotificationComparator(mContext);
final int N = extractorNames.length;
mSignalExtractors = new NotificationSignalExtractor[N];
for (int i = 0; i < N; i++) {

View File

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