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 static android.view.ViewTreeObserver.InternalInsetsInfo; 19 import static android.view.ViewTreeObserver.OnComputeInternalInsetsListener; 20 import static android.view.ViewTreeObserver.OnGlobalFocusChangeListener; 21 import static android.view.ViewTreeObserver.OnGlobalLayoutListener; 22 23 import static com.android.car.assist.client.CarAssistUtils.isCarCompatibleMessagingNotification; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.AnimatorSet; 28 import android.app.KeyguardManager; 29 import android.app.Notification; 30 import android.app.NotificationChannel; 31 import android.app.NotificationManager; 32 import android.car.drivingstate.CarUxRestrictions; 33 import android.car.drivingstate.CarUxRestrictionsManager; 34 import android.content.Context; 35 import android.service.notification.NotificationListenerService; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewTreeObserver; 41 42 import androidx.annotation.VisibleForTesting; 43 44 import com.android.car.notification.headsup.CarHeadsUpNotificationContainer; 45 import com.android.car.notification.headsup.animationhelper.HeadsUpNotificationAnimationHelper; 46 import com.android.car.notification.template.MessageNotificationViewHolder; 47 48 import java.util.ArrayList; 49 import java.util.HashMap; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Objects; 53 54 /** 55 * Notification Manager for heads-up notifications in car. 56 */ 57 public class CarHeadsUpNotificationManager 58 implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener { 59 60 /** 61 * Callback that will be issued after a Heads up notification state is changed. 62 */ 63 public interface OnHeadsUpNotificationStateChange { 64 /** 65 * Will be called if a new notification added/updated changes the heads up state for that 66 * notification. 67 */ onStateChange(AlertEntry alertEntry, boolean isHeadsUp)68 void onStateChange(AlertEntry alertEntry, boolean isHeadsUp); 69 } 70 71 private static final String TAG = CarHeadsUpNotificationManager.class.getSimpleName(); 72 73 private final Beeper mBeeper; 74 private final Context mContext; 75 private final boolean mEnableNavigationHeadsup; 76 private final long mDuration; 77 private final long mMinDisplayDuration; 78 private HeadsUpNotificationAnimationHelper mAnimationHelper; 79 private final int mNotificationHeadsUpCardMarginTop; 80 81 private final KeyguardManager mKeyguardManager; 82 private final PreprocessingManager mPreprocessingManager; 83 private final LayoutInflater mInflater; 84 private final CarHeadsUpNotificationContainer mHunContainer; 85 86 // key for the map is the statusbarnotification key 87 private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications = new HashMap<>(); 88 private final List<OnHeadsUpNotificationStateChange> mNotificationStateChangeListeners = 89 new ArrayList<>(); 90 private final Map<HeadsUpEntry, 91 Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener>> 92 mRegisteredViewTreeListeners = new HashMap<>(); 93 94 private boolean mShouldRestrictMessagePreview; 95 private NotificationClickHandlerFactory mClickHandlerFactory; 96 private NotificationDataManager mNotificationDataManager; 97 98 CarHeadsUpNotificationManager(Context context, NotificationClickHandlerFactory clickHandlerFactory, NotificationDataManager notificationDataManager, CarHeadsUpNotificationContainer hunContainer)99 public CarHeadsUpNotificationManager(Context context, 100 NotificationClickHandlerFactory clickHandlerFactory, 101 NotificationDataManager notificationDataManager, 102 CarHeadsUpNotificationContainer hunContainer) { 103 mContext = context.getApplicationContext(); 104 mEnableNavigationHeadsup = 105 context.getResources().getBoolean(R.bool.config_showNavigationHeadsup); 106 mClickHandlerFactory = clickHandlerFactory; 107 mNotificationDataManager = notificationDataManager; 108 mBeeper = new Beeper(mContext); 109 mDuration = mContext.getResources().getInteger(R.integer.headsup_notification_duration_ms); 110 mNotificationHeadsUpCardMarginTop = (int) mContext.getResources().getDimension( 111 R.dimen.headsup_notification_top_margin); 112 mMinDisplayDuration = mContext.getResources().getInteger( 113 R.integer.heads_up_notification_minimum_time); 114 mAnimationHelper = getAnimationHelper(); 115 116 mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 117 mPreprocessingManager = PreprocessingManager.getInstance(context); 118 mInflater = LayoutInflater.from(mContext); 119 mClickHandlerFactory.registerClickListener( 120 (launchResult, alertEntry) -> dismissHun(alertEntry)); 121 mHunContainer = hunContainer; 122 } 123 getAnimationHelper()124 private HeadsUpNotificationAnimationHelper getAnimationHelper() { 125 String helperName = mContext.getResources().getString( 126 R.string.config_headsUpNotificationAnimationHelper); 127 try { 128 Class<?> clazz = Class.forName(helperName); 129 return (HeadsUpNotificationAnimationHelper) clazz.getConstructor().newInstance(); 130 } catch (Exception e) { 131 throw new IllegalArgumentException( 132 String.format("Invalid animation helper: %s", helperName), e); 133 } 134 } 135 136 /** 137 * Show the notification as a heads-up if it meets the criteria. 138 * 139 * <p>Return's true if the notification will be shown as a heads up, false otherwise. 140 */ maybeShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap, Map<String, AlertEntry> activeNotifications)141 public boolean maybeShowHeadsUp( 142 AlertEntry alertEntry, 143 NotificationListenerService.RankingMap rankingMap, 144 Map<String, AlertEntry> activeNotifications) { 145 if (!shouldShowHeadsUp(alertEntry, rankingMap)) { 146 // check if this is an update to the existing notification and if it should still show 147 // as a heads up or not. 148 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 149 alertEntry.getKey()); 150 if (currentActiveHeadsUpNotification == null) { 151 return false; 152 } 153 if (CarNotificationDiff.sameNotificationKey(currentActiveHeadsUpNotification, 154 alertEntry) 155 && currentActiveHeadsUpNotification.getHandler().hasMessagesOrCallbacks()) { 156 dismissHun(alertEntry); 157 } 158 return false; 159 } 160 if (!activeNotifications.containsKey(alertEntry.getKey()) || canUpdate(alertEntry) 161 || alertAgain(alertEntry.getNotification())) { 162 showHeadsUp(mPreprocessingManager.optimizeForDriving(alertEntry), 163 rankingMap); 164 return true; 165 } 166 return false; 167 } 168 169 /** 170 * This method gets called when an app wants to cancel or withdraw its notification. 171 */ maybeRemoveHeadsUp(AlertEntry alertEntry)172 public void maybeRemoveHeadsUp(AlertEntry alertEntry) { 173 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 174 alertEntry.getKey()); 175 // if the heads up notification is already removed do nothing. 176 if (currentActiveHeadsUpNotification == null) { 177 return; 178 } 179 180 long totalDisplayDuration = 181 System.currentTimeMillis() - currentActiveHeadsUpNotification.getPostTime(); 182 // ongoing notification that has passed the minimum threshold display time. 183 if (totalDisplayDuration >= mMinDisplayDuration) { 184 removeHun(alertEntry); 185 return; 186 } 187 188 long earliestRemovalTime = mMinDisplayDuration - totalDisplayDuration; 189 190 currentActiveHeadsUpNotification.getHandler().postDelayed(() -> 191 removeHun(alertEntry), earliestRemovalTime); 192 } 193 194 /** 195 * Registers a new {@link OnHeadsUpNotificationStateChange} to the list of listeners. 196 */ registerHeadsUpNotificationStateChangeListener( OnHeadsUpNotificationStateChange listener)197 public void registerHeadsUpNotificationStateChangeListener( 198 OnHeadsUpNotificationStateChange listener) { 199 if (!mNotificationStateChangeListeners.contains(listener)) { 200 mNotificationStateChangeListeners.add(listener); 201 } 202 } 203 204 /** 205 * Unregisters a {@link OnHeadsUpNotificationStateChange} from the list of listeners. 206 */ unregisterHeadsUpNotificationStateChangeListener( OnHeadsUpNotificationStateChange listener)207 public void unregisterHeadsUpNotificationStateChangeListener( 208 OnHeadsUpNotificationStateChange listener) { 209 mNotificationStateChangeListeners.remove(listener); 210 } 211 212 /** 213 * Invokes all OnHeadsUpNotificationStateChange handlers registered in {@link 214 * OnHeadsUpNotificationStateChange}s array. 215 */ handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, boolean isHeadsUp)216 private void handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, boolean isHeadsUp) { 217 mNotificationStateChangeListeners.forEach( 218 listener -> listener.onStateChange(alertEntry, isHeadsUp)); 219 } 220 221 /** 222 * Returns true if the notification's flag is not set to 223 * {@link Notification#FLAG_ONLY_ALERT_ONCE} 224 */ alertAgain(Notification newNotification)225 private boolean alertAgain(Notification newNotification) { 226 return (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; 227 } 228 229 /** 230 * Return true if the currently displaying notification have the same key as the new added 231 * notification. In that case it will be considered as an update to the currently displayed 232 * notification. 233 */ isUpdate(AlertEntry alertEntry)234 private boolean isUpdate(AlertEntry alertEntry) { 235 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 236 alertEntry.getKey()); 237 if (currentActiveHeadsUpNotification == null) { 238 return false; 239 } 240 return CarNotificationDiff.sameNotificationKey(currentActiveHeadsUpNotification, 241 alertEntry); 242 } 243 244 /** 245 * Updates only when the notification is being displayed. 246 */ canUpdate(AlertEntry alertEntry)247 private boolean canUpdate(AlertEntry alertEntry) { 248 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 249 alertEntry.getKey()); 250 return currentActiveHeadsUpNotification != null && System.currentTimeMillis() - 251 currentActiveHeadsUpNotification.getPostTime() < mDuration; 252 } 253 254 /** 255 * Returns the active headsUpEntry or creates a new one while adding it to the list of 256 * mActiveHeadsUpNotifications. 257 */ addNewHeadsUpEntry(AlertEntry alertEntry)258 private HeadsUpEntry addNewHeadsUpEntry(AlertEntry alertEntry) { 259 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 260 alertEntry.getKey()); 261 if (currentActiveHeadsUpNotification == null) { 262 currentActiveHeadsUpNotification = new HeadsUpEntry( 263 alertEntry.getStatusBarNotification()); 264 handleHeadsUpNotificationStateChanged(alertEntry, /* isHeadsUp= */ true); 265 mActiveHeadsUpNotifications.put(alertEntry.getKey(), 266 currentActiveHeadsUpNotification); 267 currentActiveHeadsUpNotification.mIsAlertAgain = alertAgain( 268 alertEntry.getNotification()); 269 currentActiveHeadsUpNotification.mIsNewHeadsUp = true; 270 return currentActiveHeadsUpNotification; 271 } 272 currentActiveHeadsUpNotification.mIsNewHeadsUp = false; 273 currentActiveHeadsUpNotification.mIsAlertAgain = alertAgain( 274 alertEntry.getNotification()); 275 if (currentActiveHeadsUpNotification.mIsAlertAgain) { 276 // This is a ongoing notification which needs to be alerted again to the user. This 277 // requires for the post time to be updated. 278 currentActiveHeadsUpNotification.updatePostTime(); 279 } 280 return currentActiveHeadsUpNotification; 281 } 282 283 /** 284 * Controls three major conditions while showing heads up notification. 285 * <p> 286 * <ol> 287 * <li> When a new HUN comes in it will be displayed with animations 288 * <li> If an update to existing HUN comes in which enforces to alert the HUN again to user, 289 * then the post time will be updated to current time. This will only be done if {@link 290 * Notification#FLAG_ONLY_ALERT_ONCE} flag is not set. 291 * <li> If an update to existing HUN comes in which just updates the data and does not want to 292 * alert itself again, then the animations will not be shown and the data will get updated. This 293 * will only be done if {@link Notification#FLAG_ONLY_ALERT_ONCE} flag is not set. 294 * </ol> 295 */ showHeadsUp(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)296 private void showHeadsUp(AlertEntry alertEntry, 297 NotificationListenerService.RankingMap rankingMap) { 298 // Show animations only when there is no active HUN and notification is new. This check 299 // needs to be done here because after this the new notification will be added to the map 300 // holding ongoing notifications. 301 boolean shouldShowAnimation = !isUpdate(alertEntry); 302 HeadsUpEntry currentNotification = addNewHeadsUpEntry(alertEntry); 303 if (currentNotification.mIsNewHeadsUp) { 304 playSound(alertEntry, rankingMap); 305 setAutoDismissViews(currentNotification, alertEntry); 306 } else if (currentNotification.mIsAlertAgain) { 307 setAutoDismissViews(currentNotification, alertEntry); 308 } 309 CarNotificationTypeItem notificationTypeItem = NotificationUtils.getNotificationViewType( 310 alertEntry); 311 currentNotification.setClickHandlerFactory(mClickHandlerFactory); 312 313 if (currentNotification.getNotificationView() == null) { 314 currentNotification.setNotificationView(mInflater.inflate( 315 notificationTypeItem.getHeadsUpTemplate(), 316 null)); 317 mHunContainer.displayNotification(currentNotification.getNotificationView(), 318 notificationTypeItem); 319 currentNotification.setViewHolder( 320 notificationTypeItem.getViewHolder(currentNotification.getNotificationView(), 321 mClickHandlerFactory)); 322 } 323 324 currentNotification.getViewHolder().setHideDismissButton(!shouldDismissOnSwipe(alertEntry)); 325 326 if (mShouldRestrictMessagePreview && notificationTypeItem.getNotificationType() 327 == NotificationViewType.MESSAGE) { 328 ((MessageNotificationViewHolder) currentNotification.getViewHolder()) 329 .bindRestricted(alertEntry, /* isInGroup= */ false, /* isHeadsUp= */ true); 330 } else { 331 currentNotification.getViewHolder().bind(alertEntry, /* isInGroup= */false, 332 /* isHeadsUp= */ true); 333 } 334 335 resetViewTreeListenersEntry(currentNotification); 336 337 ViewTreeObserver viewTreeObserver = 338 currentNotification.getNotificationView().getViewTreeObserver(); 339 340 // measure the size of the card and make that area of the screen touchable 341 OnComputeInternalInsetsListener onComputeInternalInsetsListener = 342 info -> setInternalInsetsInfo(info, currentNotification, 343 /* panelExpanded= */ false); 344 viewTreeObserver.addOnComputeInternalInsetsListener(onComputeInternalInsetsListener); 345 // Get the height of the notification view after onLayout() in order to animate the 346 // notification into the screen. 347 viewTreeObserver.addOnGlobalLayoutListener( 348 new OnGlobalLayoutListener() { 349 @Override 350 public void onGlobalLayout() { 351 View view = currentNotification.getNotificationView(); 352 if (shouldShowAnimation) { 353 mAnimationHelper.resetHUNPosition(view); 354 AnimatorSet animatorSet = mAnimationHelper.getAnimateInAnimator( 355 mContext, view); 356 animatorSet.setTarget(view); 357 animatorSet.start(); 358 } 359 view.getViewTreeObserver().removeOnGlobalLayoutListener(this); 360 } 361 }); 362 // Reset the auto dismiss timeout for each rotary event. 363 OnGlobalFocusChangeListener onGlobalFocusChangeListener = 364 (oldFocus, newFocus) -> setAutoDismissViews(currentNotification, alertEntry); 365 viewTreeObserver.addOnGlobalFocusChangeListener(onGlobalFocusChangeListener); 366 367 mRegisteredViewTreeListeners.put(currentNotification, 368 new Pair<>(onComputeInternalInsetsListener, onGlobalFocusChangeListener)); 369 370 if (currentNotification.mIsNewHeadsUp) { 371 // Add swipe gesture 372 View cardView = currentNotification.getNotificationView().findViewById(R.id.card_view); 373 cardView.setOnTouchListener(new HeadsUpNotificationOnTouchListener(cardView, 374 shouldDismissOnSwipe(alertEntry), () -> resetView(alertEntry))); 375 376 // Add dismiss button listener 377 View dismissButton = currentNotification.getNotificationView().findViewById( 378 R.id.dismiss_button); 379 if (dismissButton != null) { 380 dismissButton.setOnClickListener(v -> dismissHun(alertEntry)); 381 } 382 } 383 } 384 resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry)385 private void resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry) { 386 Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener> listeners = 387 mRegisteredViewTreeListeners.get(headsUpEntry); 388 if (listeners == null) { 389 return; 390 } 391 392 ViewTreeObserver observer = headsUpEntry.getNotificationView().getViewTreeObserver(); 393 observer.removeOnComputeInternalInsetsListener(listeners.first); 394 observer.removeOnGlobalFocusChangeListener(listeners.second); 395 mRegisteredViewTreeListeners.remove(headsUpEntry); 396 } 397 setInternalInsetsInfo(InternalInsetsInfo info, HeadsUpEntry currentNotification, boolean panelExpanded)398 protected void setInternalInsetsInfo(InternalInsetsInfo info, 399 HeadsUpEntry currentNotification, boolean panelExpanded) { 400 // If the panel is not on screen don't modify the touch region 401 if (!mHunContainer.isVisible()) return; 402 int[] mTmpTwoArray = new int[2]; 403 View cardView = currentNotification.getNotificationView().findViewById( 404 R.id.card_view); 405 406 if (cardView == null) return; 407 408 if (panelExpanded) { 409 info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); 410 return; 411 } 412 413 cardView.getLocationInWindow(mTmpTwoArray); 414 int minX = mTmpTwoArray[0]; 415 int maxX = mTmpTwoArray[0] + cardView.getWidth(); 416 int height = cardView.getHeight(); 417 info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 418 info.touchableRegion.set(minX, mNotificationHeadsUpCardMarginTop, maxX, 419 height + mNotificationHeadsUpCardMarginTop); 420 } 421 playSound(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)422 private void playSound(AlertEntry alertEntry, 423 NotificationListenerService.RankingMap rankingMap) { 424 NotificationListenerService.Ranking ranking = getRanking(); 425 if (rankingMap.getRanking(alertEntry.getKey(), ranking)) { 426 NotificationChannel notificationChannel = ranking.getChannel(); 427 // If sound is not set on the notification channel and default is not chosen it 428 // can be null. 429 if (notificationChannel.getSound() != null) { 430 // make the sound 431 mBeeper.beep(alertEntry.getStatusBarNotification().getPackageName(), 432 notificationChannel.getSound()); 433 } 434 } 435 } 436 shouldDismissOnSwipe(AlertEntry alertEntry)437 private boolean shouldDismissOnSwipe(AlertEntry alertEntry) { 438 return !(hasFullScreenIntent(alertEntry) 439 && Objects.equals(alertEntry.getNotification().category, Notification.CATEGORY_CALL) 440 && alertEntry.getStatusBarNotification().isOngoing()); 441 } 442 443 @VisibleForTesting getActiveHeadsUpNotifications()444 protected Map<String, HeadsUpEntry> getActiveHeadsUpNotifications() { 445 return mActiveHeadsUpNotifications; 446 } 447 setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry)448 private void setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry) { 449 // Should not auto dismiss if HUN has a full screen Intent. 450 if (hasFullScreenIntent(alertEntry)) { 451 return; 452 } 453 currentNotification.getHandler().removeCallbacksAndMessages(null); 454 currentNotification.getHandler().postDelayed(() -> dismissHun(alertEntry), mDuration); 455 } 456 457 /** 458 * Returns true if AlertEntry has a full screen Intent. 459 */ hasFullScreenIntent(AlertEntry alertEntry)460 private boolean hasFullScreenIntent(AlertEntry alertEntry) { 461 return alertEntry.getNotification().fullScreenIntent != null; 462 } 463 464 /** 465 * Animates the heads up notification out of the screen and reset the views. 466 */ animateOutHun(AlertEntry alertEntry, boolean isRemoved)467 private void animateOutHun(AlertEntry alertEntry, boolean isRemoved) { 468 Log.d(TAG, "clearViews for Heads Up Notification: "); 469 // get the current notification to perform animations and remove it immediately from the 470 // active notification maps and cancel all other call backs if any. 471 HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get( 472 alertEntry.getKey()); 473 // view can also be removed when swipped away. 474 if (currentHeadsUpNotification == null) { 475 return; 476 } 477 currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null); 478 resetViewTreeListenersEntry(currentHeadsUpNotification); 479 View view = currentHeadsUpNotification.getNotificationView(); 480 481 AnimatorSet animatorSet = mAnimationHelper.getAnimateOutAnimator(mContext, view); 482 animatorSet.setTarget(view); 483 animatorSet.addListener(new AnimatorListenerAdapter() { 484 @Override 485 public void onAnimationEnd(Animator animation) { 486 mHunContainer.removeNotification(view); 487 488 // Remove HUN after the animation ends to prevent accidental touch on the card 489 // triggering another remove call. 490 mActiveHeadsUpNotifications.remove(alertEntry.getKey()); 491 492 // If the HUN was not specifically removed then add it to the panel. 493 if (!isRemoved) { 494 handleHeadsUpNotificationStateChanged(alertEntry, /* isHeadsUp= */ false); 495 } 496 } 497 }); 498 animatorSet.start(); 499 } 500 dismissHun(AlertEntry alertEntry)501 private void dismissHun(AlertEntry alertEntry) { 502 animateOutHun(alertEntry, /* isRemoved= */ false); 503 } 504 removeHun(AlertEntry alertEntry)505 private void removeHun(AlertEntry alertEntry) { 506 animateOutHun(alertEntry, /* isRemoved= */ true); 507 } 508 509 /** 510 * Removes the view for the active heads up notification and also removes the HUN from the map 511 * of active Notifications. 512 */ resetView(AlertEntry alertEntry)513 private void resetView(AlertEntry alertEntry) { 514 HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get( 515 alertEntry.getKey()); 516 if (currentHeadsUpNotification == null) return; 517 518 currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null); 519 mHunContainer.removeNotification(currentHeadsUpNotification.getNotificationView()); 520 mActiveHeadsUpNotifications.remove(alertEntry.getKey()); 521 handleHeadsUpNotificationStateChanged(alertEntry, /* isHeadsUp= */ false); 522 resetViewTreeListenersEntry(currentHeadsUpNotification); 523 } 524 525 /** 526 * Helper method that determines whether a notification should show as a heads-up. 527 * 528 * <p> A notification will never be shown as a heads-up if: 529 * <ul> 530 * <li> Keyguard (lock screen) is showing 531 * <li> OEMs configured CATEGORY_NAVIGATION should not be shown 532 * <li> Notification is muted. 533 * </ul> 534 * 535 * <p> A notification will be shown as a heads-up if: 536 * <ul> 537 * <li> Importance >= HIGH 538 * <li> it comes from an app signed with the platform key. 539 * <li> it comes from a privileged system app. 540 * <li> is a car compatible notification. 541 * {@link com.android.car.assist.client.CarAssistUtils#isCarCompatibleMessagingNotification} 542 * <li> Notification category is one of CATEGORY_CALL or CATEGORY_NAVIGATION 543 * </ul> 544 * 545 * <p> Group alert behavior still follows API documentation. 546 * 547 * @return true if a notification should be shown as a heads-up 548 */ shouldShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)549 private boolean shouldShowHeadsUp( 550 AlertEntry alertEntry, 551 NotificationListenerService.RankingMap rankingMap) { 552 if (mKeyguardManager.isKeyguardLocked()) { 553 return false; 554 } 555 Notification notification = alertEntry.getNotification(); 556 557 // Navigation notification configured by OEM 558 if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals( 559 notification.category)) { 560 return false; 561 } 562 // Group alert behavior 563 if (notification.suppressAlertingDueToGrouping()) { 564 return false; 565 } 566 // Messaging notification muted by user. 567 if (mNotificationDataManager.isMessageNotificationMuted(alertEntry)) { 568 return false; 569 } 570 571 // Do not show if importance < HIGH 572 NotificationListenerService.Ranking ranking = getRanking(); 573 if (rankingMap.getRanking(alertEntry.getKey(), ranking)) { 574 if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) { 575 return false; 576 } 577 } 578 579 if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) { 580 return true; 581 } 582 583 // Allow car messaging type. 584 if (isCarCompatibleMessagingNotification(alertEntry.getStatusBarNotification())) { 585 return true; 586 } 587 588 if (notification.category == null) { 589 Log.d(TAG, "category not set for: " 590 + alertEntry.getStatusBarNotification().getPackageName()); 591 } 592 593 // Allow for Call, and nav TBT categories. 594 return Notification.CATEGORY_CALL.equals(notification.category) 595 || Notification.CATEGORY_NAVIGATION.equals(notification.category); 596 } 597 598 @VisibleForTesting getRanking()599 protected NotificationListenerService.Ranking getRanking() { 600 return new NotificationListenerService.Ranking(); 601 } 602 603 @Override onUxRestrictionsChanged(CarUxRestrictions restrictions)604 public void onUxRestrictionsChanged(CarUxRestrictions restrictions) { 605 mShouldRestrictMessagePreview = 606 (restrictions.getActiveRestrictions() 607 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0; 608 } 609 610 /** 611 * Sets the source of {@link View.OnClickListener} 612 * 613 * @param clickHandlerFactory used to generate onClickListeners 614 */ 615 @VisibleForTesting setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)616 public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) { 617 mClickHandlerFactory = clickHandlerFactory; 618 } 619 } 620