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