blob: 9a58ccd1ec1b71327bebd5fe26995f8ce16c62bc [file] [log] [blame]
/*
* Copyright (C) 2018 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.car.notification;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationManager;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.car.notification.template.MessageNotificationViewHolder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
/**
* Manager that filters, groups and ranks the notifications in the notification center.
*
* <p> Note that heads-up notifications have a different filtering mechanism and is managed by
* {@link CarHeadsUpNotificationManager}.
*/
public class PreprocessingManager {
/** Listener that will be notified when a call state changes. **/
public interface CallStateListener {
/**
* @param isInCall is true when user is currently in a call.
*/
void onCallStateChanged(boolean isInCall);
}
private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
private static final String TAG = "PreprocessingManager";
private final String mEllipsizedSuffix;
private final Context mContext;
private final boolean mShowRecentsAndOlderHeaders;
private final boolean mUseLauncherIcon;
private final int mMinimumGroupingThreshold;
private static PreprocessingManager sInstance;
private int mMaxStringLength = Integer.MAX_VALUE;
private Map<String, AlertEntry> mOldNotifications;
private List<NotificationGroup> mOldProcessedNotifications;
private NotificationListenerService.RankingMap mOldRankingMap;
private NotificationDataManager mNotificationDataManager;
private boolean mIsInCall;
private List<CallStateListener> mCallStateListeners = new ArrayList<>();
@VisibleForTesting
final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
mIsInCall = TelephonyManager.EXTRA_STATE_OFFHOOK
.equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE));
for (CallStateListener listener : mCallStateListeners) {
listener.onCallStateChanged(mIsInCall);
}
}
}
};
private PreprocessingManager(Context context) {
mEllipsizedSuffix = context.getString(R.string.ellipsized_string);
mContext = context;
mNotificationDataManager = NotificationDataManager.getInstance();
Resources resources = mContext.getResources();
mShowRecentsAndOlderHeaders = resources.getBoolean(R.bool.config_showRecentAndOldHeaders);
mUseLauncherIcon = resources.getBoolean(R.bool.config_useLauncherIcon);
mMinimumGroupingThreshold = resources.getInteger(R.integer.config_minimumGroupingThreshold);
IntentFilter filter = new IntentFilter();
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
context.registerReceiver(mIntentReceiver, filter);
}
public static PreprocessingManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new PreprocessingManager(context);
}
return sInstance;
}
@VisibleForTesting
static void refreshInstance() {
sInstance = null;
}
@VisibleForTesting
void setNotificationDataManager(NotificationDataManager notificationDataManager) {
mNotificationDataManager = notificationDataManager;
}
/**
* Initialize the data when the UI becomes foreground.
*/
public void init(Map<String, AlertEntry> notifications, RankingMap rankingMap) {
mOldNotifications = notifications;
mOldRankingMap = rankingMap;
mOldProcessedNotifications =
process(/* showLessImportantNotifications = */ false, notifications, rankingMap);
}
/**
* Process the given notifications. In order for DiffUtil to work, the adapter needs a new
* data object each time it updates, therefore wrapping the return value in a new list.
*
* @param showLessImportantNotifications whether less important notifications should be shown.
* @param notifications the list of notifications to be processed.
* @param rankingMap the ranking map for the notifications.
* @return the processed notifications in a new list.
*/
public List<NotificationGroup> process(boolean showLessImportantNotifications,
Map<String, AlertEntry> notifications, RankingMap rankingMap) {
return new ArrayList<>(
rank(group(optimizeForDriving(
filter(showLessImportantNotifications,
new ArrayList<>(notifications.values()),
rankingMap))),
rankingMap));
}
/**
* Create a new list of notifications based on existing list.
*
* @param showLessImportantNotifications whether less important notifications should be shown.
* @param newRankingMap the latest ranking map for the notifications.
* @return the new notification group list that should be shown to the user.
*/
public List<NotificationGroup> updateNotifications(
boolean showLessImportantNotifications,
AlertEntry alertEntry,
int updateType,
RankingMap newRankingMap) {
switch (updateType) {
case CarNotificationListener.NOTIFY_NOTIFICATION_REMOVED:
// removal of a notification is the same as a normal preprocessing
mOldNotifications.remove(alertEntry.getKey());
mOldProcessedNotifications =
process(showLessImportantNotifications, mOldNotifications, mOldRankingMap);
break;
case CarNotificationListener.NOTIFY_NOTIFICATION_POSTED:
AlertEntry notification = optimizeForDriving(alertEntry);
boolean isUpdate = mOldNotifications.containsKey(notification.getKey());
mOldNotifications.put(notification.getKey(), notification);
// insert a new notification into the list
mOldProcessedNotifications = new ArrayList<>(
additionalGroupAndRank((alertEntry), newRankingMap, isUpdate));
break;
}
return mOldProcessedNotifications;
}
/** Add {@link CallStateListener} in order to be notified when call state is changed. **/
public void addCallStateListener(CallStateListener listener) {
if (mCallStateListeners.contains(listener)) return;
mCallStateListeners.add(listener);
listener.onCallStateChanged(mIsInCall);
}
/** Remove {@link CallStateListener} to stop getting notified when call state is changed. **/
public void removeCallStateListener(CallStateListener listener) {
mCallStateListeners.remove(listener);
}
/**
* Returns true if the current {@link AlertEntry} should be filtered out and not
* added to the list.
*/
boolean shouldFilter(AlertEntry alertEntry, RankingMap rankingMap) {
return isLessImportantForegroundNotification(alertEntry, rankingMap)
|| isMediaOrNavigationNotification(alertEntry);
}
/**
* Filter a list of {@link AlertEntry}s according to OEM's configurations.
*/
@VisibleForTesting
protected List<AlertEntry> filter(
boolean showLessImportantNotifications,
List<AlertEntry> notifications,
RankingMap rankingMap) {
// remove notifications that should be filtered.
if (!showLessImportantNotifications) {
notifications.removeIf(alertEntry -> shouldFilter(alertEntry, rankingMap));
}
// Call notifications should not be shown in the panel.
// Since they're shown as persistent HUNs, and notifications are not added to the panel
// until after they're dismissed as HUNs, it does not make sense to have them in the panel,
// and sequencing could cause them to be removed before being added here.
notifications.removeIf(alertEntry -> Notification.CATEGORY_CALL.equals(
alertEntry.getNotification().category));
if (DEBUG) {
Log.d(TAG, "Filtered notifications: " + notifications);
}
return notifications;
}
private boolean isLessImportantForegroundNotification(AlertEntry alertEntry,
RankingMap rankingMap) {
boolean isForeground =
(alertEntry.getNotification().flags
& Notification.FLAG_FOREGROUND_SERVICE) != 0;
if (!isForeground) {
Log.d(TAG, alertEntry + " is not a foreground notification.");
return false;
}
int importance = 0;
NotificationListenerService.Ranking ranking =
new NotificationListenerService.Ranking();
if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
importance = ranking.getImportance();
}
if (DEBUG) {
if (importance < NotificationManager.IMPORTANCE_DEFAULT) {
Log.d(TAG, alertEntry + " importance is insufficient to show in notification "
+ "center");
} else {
Log.d(TAG, alertEntry + " importance is sufficient to show in notification "
+ "center");
}
if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) {
Log.d(TAG, alertEntry + " application is system privileged or signed with "
+ "platform key");
} else {
Log.d(TAG, alertEntry + " application is neither system privileged nor signed "
+ "with platform key");
}
}
return importance < NotificationManager.IMPORTANCE_DEFAULT
&& NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry);
}
private boolean isMediaOrNavigationNotification(AlertEntry alertEntry) {
Notification notification = alertEntry.getNotification();
boolean mediaOrNav = notification.isMediaNotification()
|| Notification.CATEGORY_NAVIGATION.equals(notification.category);
if (DEBUG) {
Log.d(TAG, alertEntry + " category: " + notification.category);
}
return mediaOrNav;
}
/**
* Process a list of {@link AlertEntry}s to be driving optimized.
*
* <p> Note that the string length limit is always respected regardless of whether distraction
* optimization is required.
*/
private List<AlertEntry> optimizeForDriving(List<AlertEntry> notifications) {
notifications.forEach(notification -> notification = optimizeForDriving(notification));
return notifications;
}
/**
* Helper method that optimize a single {@link AlertEntry} for driving.
*
* <p> Currently only trimming texts that have visual effects in car. Operation is done on
* the original notification object passed in; no new object is created.
*
* <p> Note that message notifications are not trimmed, so that messages are preserved for
* assistant read-out. Instead, {@link MessageNotificationViewHolder} will be responsible
* for the presentation-level text truncation.
*/
AlertEntry optimizeForDriving(AlertEntry alertEntry) {
if (Notification.CATEGORY_MESSAGE.equals(alertEntry.getNotification().category)){
return alertEntry;
}
Bundle extras = alertEntry.getNotification().extras;
for (String key : extras.keySet()) {
switch (key) {
case Notification.EXTRA_TITLE:
case Notification.EXTRA_TEXT:
case Notification.EXTRA_TITLE_BIG:
case Notification.EXTRA_SUMMARY_TEXT:
CharSequence value = extras.getCharSequence(key);
extras.putCharSequence(key, trimText(value));
default:
continue;
}
}
return alertEntry;
}
/**
* Helper method that takes a string and trims the length to the maximum character allowed
* by the {@link CarUxRestrictionsManager}.
*/
@Nullable
public CharSequence trimText(@Nullable CharSequence text) {
if (TextUtils.isEmpty(text) || text.length() < mMaxStringLength) {
return text;
}
int maxLength = mMaxStringLength - mEllipsizedSuffix.length();
return text.toString().substring(0, maxLength) + mEllipsizedSuffix;
}
/**
* @return the maximum numbers of characters allowed by the {@link CarUxRestrictionsManager}
*/
public int getMaximumStringLength() {
return mMaxStringLength;
}
/**
* Group notifications that have the same group key.
*
* <p> Automatically generated group summaries that contains no child notifications are removed.
* This can happen if a notification group only contains less important notifications that are
* filtered out in the previous {@link #filter} step.
*
* <p> A group of child notifications without a summary notification will not be grouped.
*
* @param list list of ungrouped {@link AlertEntry}s.
* @return list of grouped notifications as {@link NotificationGroup}s.
*/
@VisibleForTesting
List<NotificationGroup> group(List<AlertEntry> list) {
SortedMap<String, NotificationGroup> groupedNotifications = new TreeMap<>();
// First pass: group all notifications according to their groupKey.
for (int i = 0; i < list.size(); i++) {
AlertEntry alertEntry = list.get(i);
Notification notification = alertEntry.getNotification();
String groupKey;
if (Notification.CATEGORY_CALL.equals(notification.category)) {
// DO NOT group CATEGORY_CALL.
groupKey = UUID.randomUUID().toString();
} else {
groupKey = alertEntry.getStatusBarNotification().getGroupKey();
}
if (!groupedNotifications.containsKey(groupKey)) {
NotificationGroup notificationGroup = new NotificationGroup();
groupedNotifications.put(groupKey, notificationGroup);
}
if (notification.isGroupSummary()) {
groupedNotifications.get(groupKey)
.setGroupSummaryNotification(alertEntry);
} else {
groupedNotifications.get(groupKey).addNotification(alertEntry);
}
}
if (DEBUG) {
Log.d(TAG, "(First pass) Grouped notifications according to groupKey: "
+ groupedNotifications);
}
// Second pass: remove automatically generated group summary if it contains no child
// notifications. This can happen if a notification group only contains less important
// notifications that are filtered out in the previous filter step.
List<NotificationGroup> groupList = new ArrayList<>(groupedNotifications.values());
groupList.removeIf(
notificationGroup -> {
AlertEntry summaryNotification =
notificationGroup.getGroupSummaryNotification();
return notificationGroup.getChildCount() == 0
&& summaryNotification != null
&& summaryNotification.getStatusBarNotification().getOverrideGroupKey()
!= null;
});
if (DEBUG) {
Log.d(TAG, "(Second pass) Remove automatically generated group summaries: "
+ groupList);
}
if (mShowRecentsAndOlderHeaders) {
mNotificationDataManager.updateUnseenNotificationGroups(groupList);
}
// Third Pass: If a notification group has seen and unseen notifications, we need to split
// up the group into its seen and unseen constituents.
List<NotificationGroup> tempGroupList = new ArrayList<>();
groupList.forEach(notificationGroup -> {
AlertEntry groupSummary = notificationGroup.getGroupSummaryNotification();
if (groupSummary == null || !mShowRecentsAndOlderHeaders) {
boolean isNotificationSeen = mNotificationDataManager
.isNotificationSeen(notificationGroup.getSingleNotification());
notificationGroup.setSeen(isNotificationSeen);
tempGroupList.add(notificationGroup);
return;
}
NotificationGroup seenNotificationGroup = new NotificationGroup();
seenNotificationGroup.setSeen(true);
seenNotificationGroup.setGroupSummaryNotification(groupSummary);
NotificationGroup unseenNotificationGroup = new NotificationGroup();
unseenNotificationGroup.setGroupSummaryNotification(groupSummary);
unseenNotificationGroup.setSeen(false);
notificationGroup.getChildNotifications().forEach(alertEntry -> {
if (mNotificationDataManager.isNotificationSeen(alertEntry)) {
seenNotificationGroup.addNotification(alertEntry);
} else {
unseenNotificationGroup.addNotification(alertEntry);
}
});
tempGroupList.add(unseenNotificationGroup);
tempGroupList.add(seenNotificationGroup);
});
groupList.clear();
groupList.addAll(tempGroupList);
if (DEBUG) {
Log.d(TAG, "(Third pass) Split notification groups by seen and unseen: "
+ groupList);
}
List<NotificationGroup> validGroupList = new ArrayList<>();
if (mUseLauncherIcon) {
// Fourth pass: since we do not use group summaries when using launcher icon, we can
// restore groups into individual notifications that do not meet grouping threshold.
groupList.forEach(
group -> {
if (group.getChildCount() < mMinimumGroupingThreshold) {
group.getChildNotifications().forEach(
notification -> {
NotificationGroup newGroup = new NotificationGroup();
newGroup.addNotification(notification);
newGroup.setSeen(group.isSeen());
validGroupList.add(newGroup);
});
} else {
validGroupList.add(group);
}
});
} else {
// Fourth pass: a notification group without a group summary or a notification group
// that do not meet grouping threshold should be restored back into individual
// notifications.
groupList.forEach(
group -> {
boolean groupWithNoGroupSummary = group.getChildCount() > 1
&& group.getGroupSummaryNotification() == null;
boolean groupWithGroupSummaryButNotEnoughNotifs =
group.getChildCount() < mMinimumGroupingThreshold
&& group.getGroupSummaryNotification() != null;
if (groupWithNoGroupSummary || groupWithGroupSummaryButNotEnoughNotifs) {
group.getChildNotifications().forEach(
notification -> {
NotificationGroup newGroup = new NotificationGroup();
newGroup.addNotification(notification);
newGroup.setSeen(group.isSeen());
validGroupList.add(newGroup);
});
} else {
validGroupList.add(group);
}
});
}
if (DEBUG) {
if (mUseLauncherIcon) {
Log.d(TAG, "(Fourth pass) Split notification groups that do not meet minimum "
+ "grouping threshold of " + mMinimumGroupingThreshold + " : "
+ validGroupList);
} else {
Log.d(TAG, "(Fourth pass) Restore notifications without group summaries and do"
+ " not meet minimum grouping threshold of " + mMinimumGroupingThreshold
+ " : " + validGroupList);
}
}
// Fifth Pass: group notifications with no child notifications should be removed.
validGroupList.removeIf(notificationGroup ->
notificationGroup.getChildNotifications().isEmpty());
if (DEBUG) {
Log.d(TAG, "(Fifth pass) Group notifications without child notifications "
+ "are removed: " + validGroupList);
}
// Sixth pass: if a notification is a group notification, update the timestamp if one of
// the children notifications shows a timestamp.
validGroupList.forEach(group -> {
if (!group.isGroup()) {
return;
}
AlertEntry groupSummaryNotification = group.getGroupSummaryNotification();
boolean showWhen = false;
long greatestTimestamp = 0;
for (AlertEntry notification : group.getChildNotifications()) {
if (notification.getNotification().showsTime()) {
showWhen = true;
greatestTimestamp = Math.max(greatestTimestamp,
notification.getNotification().when);
}
}
if (showWhen) {
groupSummaryNotification.getNotification().extras.putBoolean(
Notification.EXTRA_SHOW_WHEN, true);
groupSummaryNotification.getNotification().when = greatestTimestamp;
}
});
if (DEBUG) {
Log.d(TAG, "Grouped notifications: " + validGroupList);
}
return validGroupList;
}
/**
* Add new NotificationGroup to an existing list of NotificationGroups. The group will be
* placed above next highest ranked notification without changing the ordering of the full list.
*
* @param newNotification the {@link AlertEntry} that should be added to the list.
* @return list of grouped notifications as {@link NotificationGroup}s.
*/
@VisibleForTesting
protected List<NotificationGroup> additionalGroupAndRank(AlertEntry newNotification,
RankingMap newRankingMap, boolean isUpdate) {
Notification notification = newNotification.getNotification();
NotificationGroup newGroup = new NotificationGroup();
newGroup.setSeen(false);
if (notification.isGroupSummary()) {
// If child notifications already exist, update group summary
for (NotificationGroup oldGroup: mOldProcessedNotifications) {
if (hasSameGroupKey(oldGroup.getSingleNotification(), newNotification)) {
oldGroup.setGroupSummaryNotification(newNotification);
return mOldProcessedNotifications;
}
}
// If child notifications do not exist, insert the summary as a new notification
newGroup.setGroupSummaryNotification(newNotification);
insertRankedNotification(newGroup, newRankingMap);
return mOldProcessedNotifications;
} else {
newGroup.addNotification(newNotification);
for (int i = 0; i < mOldProcessedNotifications.size(); i++) {
NotificationGroup oldGroup = mOldProcessedNotifications.get(i);
if (TextUtils.equals(oldGroup.getGroupKey(),
newNotification.getStatusBarNotification().getGroupKey())
&& (!mShowRecentsAndOlderHeaders || !oldGroup.isSeen())) {
// If an unseen group already exists
if (oldGroup.getChildCount() == 0) {
// If a standalone group summary exists
if (isUpdate) {
// This is an update; replace the group summary notification
mOldProcessedNotifications.set(i, newGroup);
} else {
// Adding new notification; add to existing group
oldGroup.addNotification(newNotification);
mOldProcessedNotifications.set(i, oldGroup);
}
return mOldProcessedNotifications;
}
// If a group already exist with multiple children, insert outside of the group
if (isUpdate) {
oldGroup.removeNotification(newNotification);
}
oldGroup.addNotification(newNotification);
mOldProcessedNotifications.set(i, oldGroup);
return mOldProcessedNotifications;
}
}
// If it is a new notification, insert directly
insertRankedNotification(newGroup, newRankingMap);
return mOldProcessedNotifications;
}
}
// When adding a new notification we want to add it before the next highest ranked without
// changing existing order
private void insertRankedNotification(NotificationGroup group, RankingMap newRankingMap) {
NotificationListenerService.Ranking newRanking = new NotificationListenerService.Ranking();
newRankingMap.getRanking(group.getNotificationForSorting().getKey(), newRanking);
for(int i = 0; i < mOldProcessedNotifications.size(); i++) {
NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
newRankingMap.getRanking(mOldProcessedNotifications.get(
i).getNotificationForSorting().getKey(), ranking);
if (mShowRecentsAndOlderHeaders && group.isSeen()
&& !mOldProcessedNotifications.get(i).isSeen()) {
mOldProcessedNotifications.add(i, group);
return;
}
if(newRanking.getRank() < ranking.getRank()) {
mOldProcessedNotifications.add(i, group);
return;
}
}
// If it's not higher ranked than any existing notifications then just add at end
mOldProcessedNotifications.add(group);
}
private boolean hasSameGroupKey(AlertEntry notification1, AlertEntry notification2) {
return TextUtils.equals(notification1.getStatusBarNotification().getGroupKey(),
notification2.getStatusBarNotification().getGroupKey());
}
/**
* Rank notifications according to the ranking key supplied by the notification.
*/
@VisibleForTesting
protected List<NotificationGroup> rank(List<NotificationGroup> notifications,
RankingMap rankingMap) {
Collections.sort(notifications, new NotificationComparator(rankingMap));
// Rank within each group
notifications.forEach(notificationGroup -> {
if (notificationGroup.isGroup()) {
Collections.sort(
notificationGroup.getChildNotifications(),
new InGroupComparator(rankingMap));
}
});
return notifications;
}
@VisibleForTesting
protected Map getOldNotifications() {
return mOldNotifications;
}
public void setCarUxRestrictionManagerWrapper(CarUxRestrictionManagerWrapper manager) {
try {
if (manager == null || manager.getCurrentCarUxRestrictions() == null) {
return;
}
mMaxStringLength =
manager.getCurrentCarUxRestrictions().getMaxRestrictedStringLength();
} catch (RuntimeException e) {
mMaxStringLength = Integer.MAX_VALUE;
Log.e(TAG, "Failed to get UxRestrictions thus running unrestricted", e);
}
}
/**
* Comparator that sorts within the notification group by the sort key. If a sort key is not
* supplied, sort by the global ranking order.
*/
private static class InGroupComparator implements Comparator<AlertEntry> {
private final RankingMap mRankingMap;
InGroupComparator(RankingMap rankingMap) {
mRankingMap = rankingMap;
}
@Override
public int compare(AlertEntry left, AlertEntry right) {
if (left.getNotification().getSortKey() != null
&& right.getNotification().getSortKey() != null) {
return left.getNotification().getSortKey().compareTo(
right.getNotification().getSortKey());
}
NotificationListenerService.Ranking leftRanking =
new NotificationListenerService.Ranking();
mRankingMap.getRanking(left.getKey(), leftRanking);
NotificationListenerService.Ranking rightRanking =
new NotificationListenerService.Ranking();
mRankingMap.getRanking(right.getKey(), rightRanking);
return leftRanking.getRank() - rightRanking.getRank();
}
}
/**
* Comparator that sorts the notification groups by their representative notification's rank.
*/
private class NotificationComparator implements Comparator<NotificationGroup> {
private final NotificationListenerService.RankingMap mRankingMap;
NotificationComparator(NotificationListenerService.RankingMap rankingMap) {
mRankingMap = rankingMap;
}
@Override
public int compare(NotificationGroup left, NotificationGroup right) {
if (mShowRecentsAndOlderHeaders) {
if (left.isSeen() && !right.isSeen()) {
return -1;
} else if (!left.isSeen() && right.isSeen()) {
return 1;
}
}
NotificationListenerService.Ranking leftRanking =
new NotificationListenerService.Ranking();
mRankingMap.getRanking(left.getNotificationForSorting().getKey(), leftRanking);
NotificationListenerService.Ranking rightRanking =
new NotificationListenerService.Ranking();
mRankingMap.getRanking(right.getNotificationForSorting().getKey(), rightRanking);
return leftRanking.getRank() - rightRanking.getRank();
}
}
}