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