Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8732a47a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Change notification shade ordering."

parents d1c0bfd5 4a02afbf
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -5145,6 +5145,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);
+17 −13
Original line number Diff line number Diff line
@@ -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.
+160 −7
Original line number Diff line number Diff line
@@ -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));

            }
        }
    };
}
+3 −1
Original line number Diff line number Diff line
@@ -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++) {
+222 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}