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