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