1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.notification;
17 
18 import android.annotation.Nullable;
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.car.drivingstate.CarUxRestrictionsManager;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.res.Resources;
27 import android.os.Build;
28 import android.os.Bundle;
29 import android.service.notification.NotificationListenerService;
30 import android.service.notification.NotificationListenerService.RankingMap;
31 import android.telephony.TelephonyManager;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.car.notification.template.MessageNotificationViewHolder;
38 
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Comparator;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.SortedMap;
45 import java.util.TreeMap;
46 import java.util.UUID;
47 
48 /**
49  * Manager that filters, groups and ranks the notifications in the notification center.
50  *
51  * <p> Note that heads-up notifications have a different filtering mechanism and is managed by
52  * {@link CarHeadsUpNotificationManager}.
53  */
54 public class PreprocessingManager {
55 
56     /** Listener that will be notified when a call state changes. **/
57     public interface CallStateListener {
58         /**
59          * @param isInCall is true when user is currently in a call.
60          */
onCallStateChanged(boolean isInCall)61         void onCallStateChanged(boolean isInCall);
62     }
63 
64     private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
65     private static final String TAG = "PreprocessingManager";
66 
67     private final String mEllipsizedSuffix;
68     private final Context mContext;
69     private final boolean mShowRecentsAndOlderHeaders;
70     private final boolean mUseLauncherIcon;
71     private final int mMinimumGroupingThreshold;
72 
73     private static PreprocessingManager sInstance;
74 
75     private int mMaxStringLength = Integer.MAX_VALUE;
76     private Map<String, AlertEntry> mOldNotifications;
77     private List<NotificationGroup> mOldProcessedNotifications;
78     private NotificationListenerService.RankingMap mOldRankingMap;
79     private NotificationDataManager mNotificationDataManager;
80 
81     private boolean mIsInCall;
82     private List<CallStateListener> mCallStateListeners = new ArrayList<>();
83 
84     @VisibleForTesting
85     final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
86         @Override
87         public void onReceive(Context context, Intent intent) {
88             String action = intent.getAction();
89             if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
90                 mIsInCall = TelephonyManager.EXTRA_STATE_OFFHOOK
91                         .equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE));
92                 for (CallStateListener listener : mCallStateListeners) {
93                     listener.onCallStateChanged(mIsInCall);
94                 }
95             }
96         }
97     };
98 
PreprocessingManager(Context context)99     private PreprocessingManager(Context context) {
100         mEllipsizedSuffix = context.getString(R.string.ellipsized_string);
101         mContext = context;
102         mNotificationDataManager = NotificationDataManager.getInstance();
103 
104         Resources resources = mContext.getResources();
105         mShowRecentsAndOlderHeaders = resources.getBoolean(R.bool.config_showRecentAndOldHeaders);
106         mUseLauncherIcon = resources.getBoolean(R.bool.config_useLauncherIcon);
107         mMinimumGroupingThreshold = resources.getInteger(R.integer.config_minimumGroupingThreshold);
108 
109         IntentFilter filter = new IntentFilter();
110         filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
111         context.registerReceiver(mIntentReceiver, filter);
112     }
113 
getInstance(Context context)114     public static PreprocessingManager getInstance(Context context) {
115         if (sInstance == null) {
116             sInstance = new PreprocessingManager(context);
117         }
118         return sInstance;
119     }
120 
121     @VisibleForTesting
refreshInstance()122     static void refreshInstance() {
123         sInstance = null;
124     }
125 
126     @VisibleForTesting
setNotificationDataManager(NotificationDataManager notificationDataManager)127     void setNotificationDataManager(NotificationDataManager notificationDataManager) {
128         mNotificationDataManager = notificationDataManager;
129     }
130 
131     /**
132      * Initialize the data when the UI becomes foreground.
133      */
init(Map<String, AlertEntry> notifications, RankingMap rankingMap)134     public void init(Map<String, AlertEntry> notifications, RankingMap rankingMap) {
135         mOldNotifications = notifications;
136         mOldRankingMap = rankingMap;
137         mOldProcessedNotifications =
138                 process(/* showLessImportantNotifications = */ false, notifications, rankingMap);
139     }
140 
141     /**
142      * Process the given notifications. In order for DiffUtil to work, the adapter needs a new
143      * data object each time it updates, therefore wrapping the return value in a new list.
144      *
145      * @param showLessImportantNotifications whether less important notifications should be shown.
146      * @param notifications the list of notifications to be processed.
147      * @param rankingMap the ranking map for the notifications.
148      * @return the processed notifications in a new list.
149      */
process(boolean showLessImportantNotifications, Map<String, AlertEntry> notifications, RankingMap rankingMap)150     public List<NotificationGroup> process(boolean showLessImportantNotifications,
151             Map<String, AlertEntry> notifications, RankingMap rankingMap) {
152         return new ArrayList<>(
153                 rank(group(optimizeForDriving(
154                         filter(showLessImportantNotifications,
155                                 new ArrayList<>(notifications.values()),
156                                 rankingMap))),
157                         rankingMap));
158     }
159 
160     /**
161      * Create a new list of notifications based on existing list.
162      *
163      * @param showLessImportantNotifications whether less important notifications should be shown.
164      * @param newRankingMap the latest ranking map for the notifications.
165      * @return the new notification group list that should be shown to the user.
166      */
updateNotifications( boolean showLessImportantNotifications, AlertEntry alertEntry, int updateType, RankingMap newRankingMap)167     public List<NotificationGroup> updateNotifications(
168             boolean showLessImportantNotifications,
169             AlertEntry alertEntry,
170             int updateType,
171             RankingMap newRankingMap) {
172 
173         switch (updateType) {
174             case CarNotificationListener.NOTIFY_NOTIFICATION_REMOVED:
175                 // removal of a notification is the same as a normal preprocessing
176                 mOldNotifications.remove(alertEntry.getKey());
177                 mOldProcessedNotifications =
178                         process(showLessImportantNotifications, mOldNotifications, mOldRankingMap);
179                 break;
180             case CarNotificationListener.NOTIFY_NOTIFICATION_POSTED:
181                 AlertEntry notification = optimizeForDriving(alertEntry);
182                 boolean isUpdate = mOldNotifications.containsKey(notification.getKey());
183                 mOldNotifications.put(notification.getKey(), notification);
184                 // insert a new notification into the list
185                 mOldProcessedNotifications = new ArrayList<>(
186                         additionalGroupAndRank((alertEntry), newRankingMap, isUpdate));
187                 break;
188         }
189 
190         return mOldProcessedNotifications;
191     }
192 
193     /** Add {@link CallStateListener} in order to be notified when call state is changed. **/
addCallStateListener(CallStateListener listener)194     public void addCallStateListener(CallStateListener listener) {
195         if (mCallStateListeners.contains(listener)) return;
196         mCallStateListeners.add(listener);
197         listener.onCallStateChanged(mIsInCall);
198     }
199 
200     /** Remove {@link CallStateListener} to stop getting notified when call state is changed. **/
removeCallStateListener(CallStateListener listener)201     public void removeCallStateListener(CallStateListener listener) {
202         mCallStateListeners.remove(listener);
203     }
204 
205     /**
206      * Returns true if the current {@link AlertEntry} should be filtered out and not
207      * added to the list.
208      */
shouldFilter(AlertEntry alertEntry, RankingMap rankingMap)209     boolean shouldFilter(AlertEntry alertEntry, RankingMap rankingMap) {
210         return isLessImportantForegroundNotification(alertEntry, rankingMap)
211                 || isMediaOrNavigationNotification(alertEntry);
212     }
213 
214     /**
215      * Filter a list of {@link AlertEntry}s according to OEM's configurations.
216      */
217     @VisibleForTesting
filter( boolean showLessImportantNotifications, List<AlertEntry> notifications, RankingMap rankingMap)218     protected List<AlertEntry> filter(
219             boolean showLessImportantNotifications,
220             List<AlertEntry> notifications,
221             RankingMap rankingMap) {
222         // remove notifications that should be filtered.
223         if (!showLessImportantNotifications) {
224             notifications.removeIf(alertEntry -> shouldFilter(alertEntry, rankingMap));
225         }
226 
227         // Call notifications should not be shown in the panel.
228         // Since they're shown as persistent HUNs, and notifications are not added to the panel
229         // until after they're dismissed as HUNs, it does not make sense to have them in the panel,
230         // and sequencing could cause them to be removed before being added here.
231         notifications.removeIf(alertEntry -> Notification.CATEGORY_CALL.equals(
232                 alertEntry.getNotification().category));
233 
234         if (DEBUG) {
235             Log.d(TAG, "Filtered notifications: " + notifications);
236         }
237 
238         return notifications;
239     }
240 
isLessImportantForegroundNotification(AlertEntry alertEntry, RankingMap rankingMap)241     private boolean isLessImportantForegroundNotification(AlertEntry alertEntry,
242             RankingMap rankingMap) {
243         boolean isForeground =
244                 (alertEntry.getNotification().flags
245                         & Notification.FLAG_FOREGROUND_SERVICE) != 0;
246 
247         if (!isForeground) {
248             Log.d(TAG, alertEntry + " is not a foreground notification.");
249             return false;
250         }
251 
252         int importance = 0;
253         NotificationListenerService.Ranking ranking =
254                 new NotificationListenerService.Ranking();
255         if (rankingMap.getRanking(alertEntry.getKey(), ranking)) {
256             importance = ranking.getImportance();
257         }
258 
259         if (DEBUG) {
260             if (importance < NotificationManager.IMPORTANCE_DEFAULT) {
261                 Log.d(TAG, alertEntry + " importance is insufficient to show in notification "
262                         + "center");
263             } else {
264                 Log.d(TAG, alertEntry + " importance is sufficient to show in notification "
265                         + "center");
266             }
267 
268             if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) {
269                 Log.d(TAG, alertEntry + " application is system privileged or signed with "
270                         + "platform key");
271             } else {
272                 Log.d(TAG, alertEntry + " application is neither system privileged nor signed "
273                         + "with platform key");
274             }
275         }
276 
277         return importance < NotificationManager.IMPORTANCE_DEFAULT
278                 && NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry);
279     }
280 
isMediaOrNavigationNotification(AlertEntry alertEntry)281     private boolean isMediaOrNavigationNotification(AlertEntry alertEntry) {
282         Notification notification = alertEntry.getNotification();
283         boolean mediaOrNav = notification.isMediaNotification()
284                 || Notification.CATEGORY_NAVIGATION.equals(notification.category);
285         if (DEBUG) {
286             Log.d(TAG, alertEntry + " category: " + notification.category);
287         }
288         return mediaOrNav;
289     }
290 
291     /**
292      * Process a list of {@link AlertEntry}s to be driving optimized.
293      *
294      * <p> Note that the string length limit is always respected regardless of whether distraction
295      * optimization is required.
296      */
optimizeForDriving(List<AlertEntry> notifications)297     private List<AlertEntry> optimizeForDriving(List<AlertEntry> notifications) {
298         notifications.forEach(notification -> notification = optimizeForDriving(notification));
299         return notifications;
300     }
301 
302     /**
303      * Helper method that optimize a single {@link AlertEntry} for driving.
304      *
305      * <p> Currently only trimming texts that have visual effects in car. Operation is done on
306      * the original notification object passed in; no new object is created.
307      *
308      * <p> Note that message notifications are not trimmed, so that messages are preserved for
309      * assistant read-out. Instead, {@link MessageNotificationViewHolder} will be responsible
310      * for the presentation-level text truncation.
311      */
optimizeForDriving(AlertEntry alertEntry)312     AlertEntry optimizeForDriving(AlertEntry alertEntry) {
313         if (Notification.CATEGORY_MESSAGE.equals(alertEntry.getNotification().category)){
314             return alertEntry;
315         }
316 
317         Bundle extras = alertEntry.getNotification().extras;
318         for (String key : extras.keySet()) {
319             switch (key) {
320                 case Notification.EXTRA_TITLE:
321                 case Notification.EXTRA_TEXT:
322                 case Notification.EXTRA_TITLE_BIG:
323                 case Notification.EXTRA_SUMMARY_TEXT:
324                     CharSequence value = extras.getCharSequence(key);
325                     extras.putCharSequence(key, trimText(value));
326                 default:
327                     continue;
328             }
329         }
330         return alertEntry;
331     }
332 
333     /**
334      * Helper method that takes a string and trims the length to the maximum character allowed
335      * by the {@link CarUxRestrictionsManager}.
336      */
337     @Nullable
trimText(@ullable CharSequence text)338     public CharSequence trimText(@Nullable CharSequence text) {
339         if (TextUtils.isEmpty(text) || text.length() < mMaxStringLength) {
340             return text;
341         }
342         int maxLength = mMaxStringLength - mEllipsizedSuffix.length();
343         return text.toString().substring(0, maxLength) + mEllipsizedSuffix;
344     }
345 
346     /**
347      * @return the maximum numbers of characters allowed by the {@link CarUxRestrictionsManager}
348      */
getMaximumStringLength()349     public int getMaximumStringLength() {
350         return mMaxStringLength;
351     }
352 
353     /**
354      * Group notifications that have the same group key.
355      *
356      * <p> Automatically generated group summaries that contains no child notifications are removed.
357      * This can happen if a notification group only contains less important notifications that are
358      * filtered out in the previous {@link #filter} step.
359      *
360      * <p> A group of child notifications without a summary notification will not be grouped.
361      *
362      * @param list list of ungrouped {@link AlertEntry}s.
363      * @return list of grouped notifications as {@link NotificationGroup}s.
364      */
365     @VisibleForTesting
group(List<AlertEntry> list)366     List<NotificationGroup> group(List<AlertEntry> list) {
367         SortedMap<String, NotificationGroup> groupedNotifications = new TreeMap<>();
368 
369         // First pass: group all notifications according to their groupKey.
370         for (int i = 0; i < list.size(); i++) {
371             AlertEntry alertEntry = list.get(i);
372             Notification notification = alertEntry.getNotification();
373 
374             String groupKey;
375             if (Notification.CATEGORY_CALL.equals(notification.category)) {
376                 // DO NOT group CATEGORY_CALL.
377                 groupKey = UUID.randomUUID().toString();
378             } else {
379                 groupKey = alertEntry.getStatusBarNotification().getGroupKey();
380             }
381 
382             if (!groupedNotifications.containsKey(groupKey)) {
383                 NotificationGroup notificationGroup = new NotificationGroup();
384                 groupedNotifications.put(groupKey, notificationGroup);
385             }
386             if (notification.isGroupSummary()) {
387                 groupedNotifications.get(groupKey)
388                         .setGroupSummaryNotification(alertEntry);
389             } else {
390                 groupedNotifications.get(groupKey).addNotification(alertEntry);
391             }
392         }
393         if (DEBUG) {
394             Log.d(TAG, "(First pass) Grouped notifications according to groupKey: "
395                     + groupedNotifications);
396         }
397 
398         // Second pass: remove automatically generated group summary if it contains no child
399         // notifications. This can happen if a notification group only contains less important
400         // notifications that are filtered out in the previous filter step.
401         List<NotificationGroup> groupList = new ArrayList<>(groupedNotifications.values());
402         groupList.removeIf(
403                 notificationGroup -> {
404                     AlertEntry summaryNotification =
405                             notificationGroup.getGroupSummaryNotification();
406                     return notificationGroup.getChildCount() == 0
407                             && summaryNotification != null
408                             && summaryNotification.getStatusBarNotification().getOverrideGroupKey()
409                             != null;
410                 });
411         if (DEBUG) {
412             Log.d(TAG, "(Second pass) Remove automatically generated group summaries: "
413                     + groupList);
414         }
415 
416         if (mShowRecentsAndOlderHeaders) {
417             mNotificationDataManager.updateUnseenNotificationGroups(groupList);
418         }
419 
420 
421         // Third Pass: If a notification group has seen and unseen notifications, we need to split
422         // up the group into its seen and unseen constituents.
423         List<NotificationGroup> tempGroupList = new ArrayList<>();
424         groupList.forEach(notificationGroup -> {
425             AlertEntry groupSummary = notificationGroup.getGroupSummaryNotification();
426             if (groupSummary == null || !mShowRecentsAndOlderHeaders) {
427                 boolean isNotificationSeen = mNotificationDataManager
428                         .isNotificationSeen(notificationGroup.getSingleNotification());
429                 notificationGroup.setSeen(isNotificationSeen);
430                 tempGroupList.add(notificationGroup);
431                 return;
432             }
433 
434             NotificationGroup seenNotificationGroup = new NotificationGroup();
435             seenNotificationGroup.setSeen(true);
436             seenNotificationGroup.setGroupSummaryNotification(groupSummary);
437             NotificationGroup unseenNotificationGroup = new NotificationGroup();
438             unseenNotificationGroup.setGroupSummaryNotification(groupSummary);
439             unseenNotificationGroup.setSeen(false);
440 
441             notificationGroup.getChildNotifications().forEach(alertEntry -> {
442                 if (mNotificationDataManager.isNotificationSeen(alertEntry)) {
443                     seenNotificationGroup.addNotification(alertEntry);
444                 } else {
445                     unseenNotificationGroup.addNotification(alertEntry);
446                 }
447             });
448             tempGroupList.add(unseenNotificationGroup);
449             tempGroupList.add(seenNotificationGroup);
450         });
451         groupList.clear();
452         groupList.addAll(tempGroupList);
453         if (DEBUG) {
454             Log.d(TAG, "(Third pass) Split notification groups by seen and unseen: "
455                     + groupList);
456         }
457 
458         List<NotificationGroup> validGroupList = new ArrayList<>();
459         if (mUseLauncherIcon) {
460             // Fourth pass: since we do not use group summaries when using launcher icon, we can
461             // restore groups into individual notifications that do not meet grouping threshold.
462             groupList.forEach(
463                     group -> {
464                         if (group.getChildCount() < mMinimumGroupingThreshold) {
465                             group.getChildNotifications().forEach(
466                                     notification -> {
467                                         NotificationGroup newGroup = new NotificationGroup();
468                                         newGroup.addNotification(notification);
469                                         newGroup.setSeen(group.isSeen());
470                                         validGroupList.add(newGroup);
471                                     });
472                         } else {
473                             validGroupList.add(group);
474                         }
475                     });
476         } else {
477             // Fourth pass: a notification group without a group summary or a notification group
478             // that do not meet grouping threshold should be restored back into individual
479             // notifications.
480             groupList.forEach(
481                     group -> {
482                         boolean groupWithNoGroupSummary = group.getChildCount() > 1
483                                 && group.getGroupSummaryNotification() == null;
484                         boolean groupWithGroupSummaryButNotEnoughNotifs =
485                                 group.getChildCount() < mMinimumGroupingThreshold
486                                         && group.getGroupSummaryNotification() != null;
487                         if (groupWithNoGroupSummary || groupWithGroupSummaryButNotEnoughNotifs) {
488                             group.getChildNotifications().forEach(
489                                     notification -> {
490                                         NotificationGroup newGroup = new NotificationGroup();
491                                         newGroup.addNotification(notification);
492                                         newGroup.setSeen(group.isSeen());
493                                         validGroupList.add(newGroup);
494                                     });
495                         } else {
496                             validGroupList.add(group);
497                         }
498                     });
499         }
500         if (DEBUG) {
501             if (mUseLauncherIcon) {
502                 Log.d(TAG, "(Fourth pass) Split notification groups that do not meet minimum "
503                         + "grouping threshold of " + mMinimumGroupingThreshold + " : "
504                         + validGroupList);
505             } else {
506                 Log.d(TAG, "(Fourth pass) Restore notifications without group summaries and do"
507                         + " not meet minimum grouping threshold of " + mMinimumGroupingThreshold
508                         + " : " + validGroupList);
509             }
510         }
511 
512 
513         // Fifth Pass: group notifications with no child notifications should be removed.
514         validGroupList.removeIf(notificationGroup ->
515                 notificationGroup.getChildNotifications().isEmpty());
516         if (DEBUG) {
517             Log.d(TAG, "(Fifth pass) Group notifications without child notifications "
518                     + "are removed: " + validGroupList);
519         }
520 
521         // Sixth pass: if a notification is a group notification, update the timestamp if one of
522         // the children notifications shows a timestamp.
523         validGroupList.forEach(group -> {
524             if (!group.isGroup()) {
525                 return;
526             }
527 
528             AlertEntry groupSummaryNotification = group.getGroupSummaryNotification();
529             boolean showWhen = false;
530             long greatestTimestamp = 0;
531             for (AlertEntry notification : group.getChildNotifications()) {
532                 if (notification.getNotification().showsTime()) {
533                     showWhen = true;
534                     greatestTimestamp = Math.max(greatestTimestamp,
535                             notification.getNotification().when);
536                 }
537             }
538 
539             if (showWhen) {
540                 groupSummaryNotification.getNotification().extras.putBoolean(
541                         Notification.EXTRA_SHOW_WHEN, true);
542                 groupSummaryNotification.getNotification().when = greatestTimestamp;
543             }
544         });
545         if (DEBUG) {
546             Log.d(TAG, "Grouped notifications: " + validGroupList);
547         }
548 
549         return validGroupList;
550     }
551 
552     /**
553      * Add new NotificationGroup to an existing list of NotificationGroups. The group will be
554      * placed above next highest ranked notification without changing the ordering of the full list.
555      *
556      * @param newNotification the {@link AlertEntry} that should be added to the list.
557      * @return list of grouped notifications as {@link NotificationGroup}s.
558      */
559     @VisibleForTesting
additionalGroupAndRank(AlertEntry newNotification, RankingMap newRankingMap, boolean isUpdate)560     protected List<NotificationGroup> additionalGroupAndRank(AlertEntry newNotification,
561             RankingMap newRankingMap, boolean isUpdate) {
562         Notification notification = newNotification.getNotification();
563         NotificationGroup newGroup = new NotificationGroup();
564         newGroup.setSeen(false);
565 
566         if (notification.isGroupSummary()) {
567             // If child notifications already exist, update group summary
568             for (NotificationGroup oldGroup: mOldProcessedNotifications) {
569                 if (hasSameGroupKey(oldGroup.getSingleNotification(), newNotification)) {
570                     oldGroup.setGroupSummaryNotification(newNotification);
571                     return mOldProcessedNotifications;
572                 }
573             }
574             // If child notifications do not exist, insert the summary as a new notification
575             newGroup.setGroupSummaryNotification(newNotification);
576             insertRankedNotification(newGroup, newRankingMap);
577             return mOldProcessedNotifications;
578         } else {
579             newGroup.addNotification(newNotification);
580             for (int i = 0; i < mOldProcessedNotifications.size(); i++) {
581                 NotificationGroup oldGroup = mOldProcessedNotifications.get(i);
582                 if (TextUtils.equals(oldGroup.getGroupKey(),
583                         newNotification.getStatusBarNotification().getGroupKey())
584                         && (!mShowRecentsAndOlderHeaders || !oldGroup.isSeen())) {
585                     // If an unseen group already exists
586                     if (oldGroup.getChildCount() == 0) {
587                         // If a standalone group summary exists
588                         if (isUpdate) {
589                             // This is an update; replace the group summary notification
590                             mOldProcessedNotifications.set(i, newGroup);
591                         } else {
592                             // Adding new notification; add to existing group
593                             oldGroup.addNotification(newNotification);
594                             mOldProcessedNotifications.set(i, oldGroup);
595                         }
596                         return mOldProcessedNotifications;
597                     }
598                     // If a group already exist with multiple children, insert outside of the group
599                     if (isUpdate) {
600                         oldGroup.removeNotification(newNotification);
601                     }
602                     oldGroup.addNotification(newNotification);
603                     mOldProcessedNotifications.set(i, oldGroup);
604                     return mOldProcessedNotifications;
605                 }
606             }
607             // If it is a new notification, insert directly
608             insertRankedNotification(newGroup, newRankingMap);
609             return mOldProcessedNotifications;
610         }
611     }
612 
613     // When adding a new notification we want to add it before the next highest ranked without
614     // changing existing order
insertRankedNotification(NotificationGroup group, RankingMap newRankingMap)615     private void insertRankedNotification(NotificationGroup group, RankingMap newRankingMap) {
616         NotificationListenerService.Ranking newRanking = new NotificationListenerService.Ranking();
617         newRankingMap.getRanking(group.getNotificationForSorting().getKey(), newRanking);
618 
619         for(int i = 0; i < mOldProcessedNotifications.size(); i++) {
620             NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
621             newRankingMap.getRanking(mOldProcessedNotifications.get(
622                     i).getNotificationForSorting().getKey(), ranking);
623             if (mShowRecentsAndOlderHeaders && group.isSeen()
624                     && !mOldProcessedNotifications.get(i).isSeen()) {
625                 mOldProcessedNotifications.add(i, group);
626                 return;
627             }
628 
629             if(newRanking.getRank() < ranking.getRank()) {
630                 mOldProcessedNotifications.add(i, group);
631                 return;
632             }
633         }
634 
635         // If it's not higher ranked than any existing notifications then just add at end
636         mOldProcessedNotifications.add(group);
637     }
638 
hasSameGroupKey(AlertEntry notification1, AlertEntry notification2)639     private boolean hasSameGroupKey(AlertEntry notification1, AlertEntry notification2) {
640         return TextUtils.equals(notification1.getStatusBarNotification().getGroupKey(),
641                 notification2.getStatusBarNotification().getGroupKey());
642     }
643 
644     /**
645      * Rank notifications according to the ranking key supplied by the notification.
646      */
647     @VisibleForTesting
rank(List<NotificationGroup> notifications, RankingMap rankingMap)648     protected List<NotificationGroup> rank(List<NotificationGroup> notifications,
649             RankingMap rankingMap) {
650 
651         Collections.sort(notifications, new NotificationComparator(rankingMap));
652 
653         // Rank within each group
654         notifications.forEach(notificationGroup -> {
655             if (notificationGroup.isGroup()) {
656                 Collections.sort(
657                         notificationGroup.getChildNotifications(),
658                         new InGroupComparator(rankingMap));
659             }
660         });
661         return notifications;
662     }
663 
664     @VisibleForTesting
getOldNotifications()665     protected Map getOldNotifications() {
666         return mOldNotifications;
667     }
668 
setCarUxRestrictionManagerWrapper(CarUxRestrictionManagerWrapper manager)669     public void setCarUxRestrictionManagerWrapper(CarUxRestrictionManagerWrapper manager) {
670         try {
671             if (manager == null || manager.getCurrentCarUxRestrictions() == null) {
672                 return;
673             }
674             mMaxStringLength =
675                     manager.getCurrentCarUxRestrictions().getMaxRestrictedStringLength();
676         } catch (RuntimeException e) {
677             mMaxStringLength = Integer.MAX_VALUE;
678             Log.e(TAG, "Failed to get UxRestrictions thus running unrestricted", e);
679         }
680     }
681 
682     /**
683      * Comparator that sorts within the notification group by the sort key. If a sort key is not
684      * supplied, sort by the global ranking order.
685      */
686     private static class InGroupComparator implements Comparator<AlertEntry> {
687         private final RankingMap mRankingMap;
688 
InGroupComparator(RankingMap rankingMap)689         InGroupComparator(RankingMap rankingMap) {
690             mRankingMap = rankingMap;
691         }
692 
693         @Override
compare(AlertEntry left, AlertEntry right)694         public int compare(AlertEntry left, AlertEntry right) {
695             if (left.getNotification().getSortKey() != null
696                     && right.getNotification().getSortKey() != null) {
697                 return left.getNotification().getSortKey().compareTo(
698                         right.getNotification().getSortKey());
699             }
700 
701             NotificationListenerService.Ranking leftRanking =
702                     new NotificationListenerService.Ranking();
703             mRankingMap.getRanking(left.getKey(), leftRanking);
704 
705             NotificationListenerService.Ranking rightRanking =
706                     new NotificationListenerService.Ranking();
707             mRankingMap.getRanking(right.getKey(), rightRanking);
708 
709             return leftRanking.getRank() - rightRanking.getRank();
710         }
711     }
712 
713     /**
714      * Comparator that sorts the notification groups by their representative notification's rank.
715      */
716     private class NotificationComparator implements Comparator<NotificationGroup> {
717         private final NotificationListenerService.RankingMap mRankingMap;
718 
NotificationComparator(NotificationListenerService.RankingMap rankingMap)719         NotificationComparator(NotificationListenerService.RankingMap rankingMap) {
720             mRankingMap = rankingMap;
721         }
722 
723         @Override
compare(NotificationGroup left, NotificationGroup right)724         public int compare(NotificationGroup left, NotificationGroup right) {
725             if (mShowRecentsAndOlderHeaders) {
726                 if (left.isSeen() && !right.isSeen()) {
727                     return -1;
728                 } else if (!left.isSeen() && right.isSeen()) {
729                     return 1;
730                 }
731             }
732 
733             NotificationListenerService.Ranking leftRanking =
734                     new NotificationListenerService.Ranking();
735             mRankingMap.getRanking(left.getNotificationForSorting().getKey(), leftRanking);
736 
737             NotificationListenerService.Ranking rightRanking =
738                     new NotificationListenerService.Ranking();
739             mRankingMap.getRanking(right.getNotificationForSorting().getKey(), rightRanking);
740 
741             return leftRanking.getRank() - rightRanking.getRank();
742         }
743     }
744 }
745