diff --git a/api/system-current.txt b/api/system-current.txt index d8eff57adc91f..d3f418c94aace 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -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 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 c1e20726dff80..119b0553d0541 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3985,19 +3985,6 @@ public class Notification implements Parcelable return new Builder(builderContext, n); } - private static Class getNotificationStyleClass(String templateClass) { - Class[] classes = new Class[] { - BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, - DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, - MessagingStyle.class }; - for (Class 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 getNotificationStyleClass(String templateClass) { + Class[] classes = new Class[] { + BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, + DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, + MessagingStyle.class }; + for (Class 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 7dff2c1bdd23e..1e0035d65939a 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 @@ import java.util.Comparator; public class NotificationComparator implements Comparator { + private final String DEFAULT_SMS_APP_SETTING = Settings.Secure.SMS_DEFAULT_APPLICATION; + + private final Context mContext; + private String mDefaultPhoneApp; + private ArrayMap 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 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 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 89101a80ea12a..95718de6e2c71 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 @@ 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 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++) { 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 0000000000000..403b65c44a2f5 --- /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 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 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); + } +}