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.ActivityTaskManager; 29 import android.app.KeyguardManager; 30 import android.app.Notification; 31 import android.app.NotificationChannel; 32 import android.app.NotificationManager; 33 import android.car.drivingstate.CarUxRestrictions; 34 import android.car.drivingstate.CarUxRestrictionsManager; 35 import android.content.Context; 36 import android.os.Build; 37 import android.service.notification.NotificationListenerService; 38 import android.util.Log; 39 import android.util.Pair; 40 import android.view.LayoutInflater; 41 import android.view.View; 42 import android.view.ViewTreeObserver; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.annotation.UiThread; 47 import androidx.annotation.VisibleForTesting; 48 49 import com.android.car.notification.headsup.CarHeadsUpNotificationContainer; 50 import com.android.car.notification.headsup.animationhelper.HeadsUpNotificationAnimationHelper; 51 import com.android.car.notification.template.MessageNotificationViewHolder; 52 53 import java.time.Clock; 54 import java.util.ArrayList; 55 import java.util.HashMap; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Objects; 59 import java.util.Set; 60 import java.util.concurrent.ConcurrentHashMap; 61 import java.util.concurrent.ScheduledThreadPoolExecutor; 62 63 /** 64 * Notification Manager for heads-up notifications in car. 65 */ 66 public class CarHeadsUpNotificationManager 67 implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener { 68 69 /** 70 * Callback that will be issued after a Heads up notification state is changed. 71 */ 72 public interface OnHeadsUpNotificationStateChange { 73 /** 74 * Will be called if a new notification added/updated changes the heads up state for that 75 * notification. 76 */ onStateChange(AlertEntry alertEntry, HeadsUpState headsUpState)77 void onStateChange(AlertEntry alertEntry, HeadsUpState headsUpState); 78 } 79 80 /** 81 * Captures HUN State with following values: 82 * <ul> 83 * {@link HeadsUpState.SHOWN}: shown on screen 84 * {@link HeadsUpState.DISMISSED}: HUN dismissed by user/timeout/system and no longer displayed 85 * {@link HeadsUpState.REMOVED_FROM_QUEUE}: removed from {@link CarHeadsUpNotificationQueue} 86 * without displaying 87 * {@link HeadsUpState.REMOVED_BY_SENDER}: HUN dismissed because it was removed by the sender 88 * app 89 * <ul/> 90 */ 91 public enum HeadsUpState { 92 SHOWN, 93 DISMISSED, 94 REMOVED_FROM_QUEUE, 95 REMOVED_BY_SENDER 96 } 97 98 private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG; 99 private static final String TAG = CarHeadsUpNotificationManager.class.getSimpleName(); 100 101 private final Beeper mBeeper; 102 private final Context mContext; 103 private final boolean mEnableNavigationHeadsup; 104 private final long mDuration; 105 private final long mMinDisplayDuration; 106 private HeadsUpNotificationAnimationHelper mAnimationHelper; 107 private final int mNotificationHeadsUpCardMarginTop; 108 private final boolean mIsSuppressAndThrottleHeadsUp; 109 110 private final KeyguardManager mKeyguardManager; 111 private final PreprocessingManager mPreprocessingManager; 112 private final LayoutInflater mInflater; 113 @VisibleForTesting 114 final CarHeadsUpNotificationContainer mHunContainer; 115 private final CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback 116 mCarHeadsUpNotificationQueueCallback; 117 118 /** 119 * Mapping key({@link AlertEntry#getKey}) to active HUNs({@link HeadsUpEntry}). 120 */ 121 private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications = new ConcurrentHashMap<>(); 122 123 /** 124 * Set of key({@link AlertEntry#getKey}) of HUNs that are currently in the process of being 125 * dismissed. 126 */ 127 private final Set<String> mDismissingHeadsUpNotifications = ConcurrentHashMap.newKeySet(); 128 129 /** 130 * Set of key({@link AlertEntry#getKey}) of HUNs that are removed by the sender. 131 */ 132 private final Set<String> mHeadsUpNotificationsToBeRemoved = ConcurrentHashMap.newKeySet(); 133 private final List<OnHeadsUpNotificationStateChange> mNotificationStateChangeListeners = 134 new ArrayList<>(); 135 private final Map<HeadsUpEntry, 136 Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener>> 137 mRegisteredViewTreeListeners = new HashMap<>(); 138 139 private boolean mShouldRestrictMessagePreview; 140 private NotificationClickHandlerFactory mClickHandlerFactory; 141 private NotificationDataManager mNotificationDataManager; 142 private CarHeadsUpNotificationQueue mCarHeadsUpNotificationQueue; 143 private Clock mClock; 144 CarHeadsUpNotificationManager(Context context, NotificationClickHandlerFactory clickHandlerFactory, CarHeadsUpNotificationContainer hunContainer)145 public CarHeadsUpNotificationManager(Context context, 146 NotificationClickHandlerFactory clickHandlerFactory, 147 CarHeadsUpNotificationContainer hunContainer) { 148 mContext = context.getApplicationContext(); 149 mEnableNavigationHeadsup = 150 context.getResources().getBoolean(R.bool.config_showNavigationHeadsup); 151 mClickHandlerFactory = clickHandlerFactory; 152 mNotificationDataManager = NotificationDataManager.getInstance(); 153 mBeeper = new Beeper(mContext); 154 mDuration = mContext.getResources().getInteger(R.integer.headsup_notification_duration_ms); 155 mNotificationHeadsUpCardMarginTop = (int) mContext.getResources().getDimension( 156 R.dimen.headsup_notification_top_margin); 157 mMinDisplayDuration = mContext.getResources().getInteger( 158 R.integer.heads_up_notification_minimum_time); 159 mAnimationHelper = getAnimationHelper(); 160 161 mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 162 mPreprocessingManager = PreprocessingManager.getInstance(context); 163 mInflater = LayoutInflater.from(mContext); 164 mClickHandlerFactory.registerClickListener((launchResult, alertEntry) -> { 165 if (isActiveHun(alertEntry)) { 166 dismissHun(alertEntry, /* shouldAnimate= */ true); 167 } 168 }); 169 mClickHandlerFactory.setHunDismissCallback( 170 (launchResult, alertEntry) -> dismissHun(alertEntry, /* shouldAnimate= */ true)); 171 mHunContainer = hunContainer; 172 mIsSuppressAndThrottleHeadsUp = context.getResources().getBoolean( 173 R.bool.config_suppressAndThrottleHeadsUp); 174 mClock = Clock.systemUTC(); 175 mCarHeadsUpNotificationQueueCallback = 176 new CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback() { 177 @Override 178 public void showAsHeadsUp(AlertEntry alertEntry, 179 NotificationListenerService.RankingMap rankingMap) { 180 mContext.getMainExecutor().execute(() -> showHeadsUp( 181 mPreprocessingManager.optimizeForDriving(alertEntry), 182 rankingMap) 183 ); 184 } 185 186 @Override 187 public void removedFromHeadsUpQueue(AlertEntry alertEntry) { 188 handleHeadsUpNotificationStateChanged(alertEntry, 189 HeadsUpState.REMOVED_FROM_QUEUE); 190 } 191 192 @Override 193 public void dismissHeadsUp(@Nullable AlertEntry alertEntry) { 194 if (alertEntry != null) { 195 dismissHun(alertEntry, /* shouldAnimate= */ true); 196 } 197 } 198 199 @Override 200 public List<AlertEntry> getActiveHeadsUpNotifications() { 201 return new ArrayList<>(mActiveHeadsUpNotifications.values()); 202 } 203 }; 204 mCarHeadsUpNotificationQueue = new CarHeadsUpNotificationQueue(context, 205 ActivityTaskManager.getInstance(), 206 mContext.getSystemService(NotificationManager.class), 207 new ScheduledThreadPoolExecutor(/* corePoolSize= */ 1), 208 mCarHeadsUpNotificationQueueCallback 209 ); 210 registerHeadsUpNotificationStateChangeListener(mCarHeadsUpNotificationQueue); 211 } 212 213 @VisibleForTesting setNotificationDataManager(NotificationDataManager notificationDataManager)214 void setNotificationDataManager(NotificationDataManager notificationDataManager) { 215 mNotificationDataManager = notificationDataManager; 216 } 217 getAnimationHelper()218 private HeadsUpNotificationAnimationHelper getAnimationHelper() { 219 String helperName = mContext.getResources().getString( 220 R.string.config_headsUpNotificationAnimationHelper); 221 try { 222 Class<?> clazz = Class.forName(helperName); 223 return (HeadsUpNotificationAnimationHelper) clazz.getConstructor().newInstance(); 224 } catch (Exception e) { 225 throw new IllegalArgumentException( 226 String.format("Invalid animation helper: %s", helperName), e); 227 } 228 } 229 230 /** 231 * Show the notification as a heads-up if it meets the criteria. 232 * 233 * <p>Return's true if the notification will be shown as a heads up, false otherwise. 234 */ maybeShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap, Map<String, AlertEntry> activeNotifications)235 public boolean maybeShowHeadsUp( 236 AlertEntry alertEntry, 237 NotificationListenerService.RankingMap rankingMap, 238 Map<String, AlertEntry> activeNotifications) { 239 if (!shouldShowHeadsUp(alertEntry, rankingMap)) { 240 if (!isActiveHun(alertEntry)) { 241 if (DEBUG) { 242 Log.d(TAG, alertEntry + " is not an active heads up notification"); 243 } 244 return false; 245 } 246 // Check if this is an update to the existing notification and if it should still show 247 // as a heads up or not. 248 HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); 249 if (CarNotificationDiff.sameNotificationKey(currentActiveHeadsUpNotification, 250 alertEntry) 251 && currentActiveHeadsUpNotification.getHandler().hasMessagesOrCallbacks()) { 252 dismissHun(alertEntry, /* shouldAnimate= */ true); 253 } 254 return false; 255 } 256 boolean containsKeyFlag = !activeNotifications.containsKey(alertEntry.getKey()); 257 boolean canUpdateFlag = canUpdate(alertEntry); 258 boolean alertAgainFlag = alertAgain(alertEntry.getNotification()); 259 if (DEBUG) { 260 Log.d(TAG, alertEntry + " is an active notification: " + containsKeyFlag); 261 Log.d(TAG, alertEntry + " is an updatable notification: " + canUpdateFlag); 262 Log.d(TAG, alertEntry + " is not an alert once notification: " + alertAgainFlag); 263 } 264 if (canUpdateFlag) { 265 showHeadsUp(mPreprocessingManager.optimizeForDriving(alertEntry), 266 rankingMap); 267 return true; 268 } else if (containsKeyFlag || alertAgainFlag) { 269 if (!mIsSuppressAndThrottleHeadsUp) { 270 showHeadsUp(mPreprocessingManager.optimizeForDriving(alertEntry), 271 rankingMap); 272 } else { 273 mCarHeadsUpNotificationQueue.addToQueue(alertEntry, rankingMap); 274 } 275 return true; 276 } 277 return false; 278 } 279 280 /** 281 * This method gets called when an app wants to cancel or withdraw its notification. 282 */ maybeRemoveHeadsUp(AlertEntry alertEntry)283 public void maybeRemoveHeadsUp(AlertEntry alertEntry) { 284 if (mCarHeadsUpNotificationQueue.removeFromQueue(alertEntry)) { 285 return; 286 } 287 288 if (!isActiveHun(alertEntry)) { 289 // If the heads up notification is already removed do nothing. 290 return; 291 } 292 tagCurrentActiveHunToBeRemoved(alertEntry); 293 294 scheduleRemoveHeadsUp(alertEntry); 295 } 296 297 /** 298 * Release the notifications stored in the queue. 299 */ releaseQueue()300 public void releaseQueue() { 301 mCarHeadsUpNotificationQueue.releaseQueue(); 302 } 303 304 /** 305 * Clears all local cached variables and gracefully removes any heads up notification views if 306 * present. 307 */ clearCache()308 public void clearCache() { 309 mCarHeadsUpNotificationQueue.clearCache(); 310 for (AlertEntry alertEntry : mActiveHeadsUpNotifications.values()) { 311 dismissHun(alertEntry, /* shouldAnimate= */ false); 312 } 313 } 314 scheduleRemoveHeadsUp(AlertEntry alertEntry)315 private void scheduleRemoveHeadsUp(AlertEntry alertEntry) { 316 HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); 317 318 long totalDisplayDuration = 319 mClock.millis() - currentActiveHeadsUpNotification.getPostTime(); 320 // ongoing notification that has passed the minimum threshold display time. 321 if (totalDisplayDuration >= mMinDisplayDuration) { 322 dismissHun(alertEntry, /* shouldAnimate= */ true); 323 return; 324 } 325 326 long earliestRemovalTime = mMinDisplayDuration - totalDisplayDuration; 327 328 currentActiveHeadsUpNotification.getHandler().postDelayed( 329 () -> dismissHun(alertEntry, /* shouldAnimate= */ true), earliestRemovalTime); 330 } 331 332 /** 333 * Registers a new {@link OnHeadsUpNotificationStateChange} to the list of listeners. 334 */ registerHeadsUpNotificationStateChangeListener( OnHeadsUpNotificationStateChange listener)335 public void registerHeadsUpNotificationStateChangeListener( 336 OnHeadsUpNotificationStateChange listener) { 337 if (!mNotificationStateChangeListeners.contains(listener)) { 338 mNotificationStateChangeListeners.add(listener); 339 } 340 } 341 342 /** 343 * Unregisters all {@link OnHeadsUpNotificationStateChange} listeners along with other listeners 344 * registered by {@link CarHeadsUpNotificationManager}. 345 */ unregisterListeners()346 public void unregisterListeners() { 347 mNotificationStateChangeListeners.clear(); 348 mCarHeadsUpNotificationQueue.unregisterListeners(); 349 } 350 351 /** 352 * Invokes all OnHeadsUpNotificationStateChange handlers registered in {@link 353 * OnHeadsUpNotificationStateChange}s array. 354 */ handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, HeadsUpState headsUpState)355 private void handleHeadsUpNotificationStateChanged(AlertEntry alertEntry, 356 HeadsUpState headsUpState) { 357 mNotificationStateChangeListeners.forEach( 358 listener -> listener.onStateChange(alertEntry, headsUpState)); 359 } 360 361 /** 362 * Returns true if the notification's flag is not set to 363 * {@link Notification#FLAG_ONLY_ALERT_ONCE} 364 */ alertAgain(Notification newNotification)365 private boolean alertAgain(Notification newNotification) { 366 return (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; 367 } 368 369 /** 370 * Return true if the currently displaying notification have the same key as the new added 371 * notification. In that case it will be considered as an update to the currently displayed 372 * notification. 373 */ isUpdate(AlertEntry alertEntry)374 private boolean isUpdate(AlertEntry alertEntry) { 375 return isActiveHun(alertEntry) && CarNotificationDiff.sameNotificationKey( 376 getActiveHeadsUpEntry(alertEntry), alertEntry); 377 } 378 379 /** 380 * Updates only when the notification is being displayed. 381 */ canUpdate(AlertEntry alertEntry)382 private boolean canUpdate(AlertEntry alertEntry) { 383 return isActiveHun(alertEntry) && (System.currentTimeMillis() 384 - getActiveHeadsUpEntry(alertEntry).getPostTime()) < mDuration; 385 } 386 387 /** 388 * Returns the active headsUpEntry or creates a new one while adding it to the list of 389 * mActiveHeadsUpNotifications. 390 */ addNewHeadsUpEntry(AlertEntry alertEntry)391 private HeadsUpEntry addNewHeadsUpEntry(AlertEntry alertEntry) { 392 if (!isActiveHun(alertEntry)) { 393 HeadsUpEntry newActiveHeadsUpNotification = new HeadsUpEntry( 394 alertEntry.getStatusBarNotification()); 395 handleHeadsUpNotificationStateChanged(alertEntry, HeadsUpState.SHOWN); 396 mActiveHeadsUpNotifications.put(alertEntry.getKey(), 397 newActiveHeadsUpNotification); 398 newActiveHeadsUpNotification.mIsAlertAgain = alertAgain( 399 alertEntry.getNotification()); 400 newActiveHeadsUpNotification.mIsNewHeadsUp = true; 401 return newActiveHeadsUpNotification; 402 } 403 HeadsUpEntry currentActiveHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); 404 currentActiveHeadsUpNotification.mIsNewHeadsUp = false; 405 currentActiveHeadsUpNotification.mIsAlertAgain = alertAgain( 406 alertEntry.getNotification()); 407 if (currentActiveHeadsUpNotification.mIsAlertAgain) { 408 // This is a ongoing notification which needs to be alerted again to the user. This 409 // requires for the post time to be updated. 410 currentActiveHeadsUpNotification.updatePostTime(); 411 } 412 return currentActiveHeadsUpNotification; 413 } 414 415 /** 416 * Controls three major conditions while showing heads up notification. 417 * <p> 418 * <ol> 419 * <li> When a new HUN comes in it will be displayed with animations 420 * <li> If an update to existing HUN comes in which enforces to alert the HUN again to user, 421 * then the post time will be updated to current time. This will only be done if {@link 422 * Notification#FLAG_ONLY_ALERT_ONCE} flag is not set. 423 * <li> If an update to existing HUN comes in which just updates the data and does not want to 424 * alert itself again, then the animations will not be shown and the data will get updated. This 425 * will only be done if {@link Notification#FLAG_ONLY_ALERT_ONCE} flag is not set. 426 * </ol> 427 */ 428 @UiThread showHeadsUp(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)429 private void showHeadsUp(AlertEntry alertEntry, 430 NotificationListenerService.RankingMap rankingMap) { 431 // Show animations only when there is no active HUN and notification is new. This check 432 // needs to be done here because after this the new notification will be added to the map 433 // holding ongoing notifications. 434 boolean shouldShowAnimation = !isUpdate(alertEntry); 435 HeadsUpEntry currentNotification = addNewHeadsUpEntry(alertEntry); 436 if (currentNotification.mIsNewHeadsUp) { 437 playSound(alertEntry, rankingMap); 438 setAutoDismissViews(currentNotification, alertEntry); 439 } else if (currentNotification.mIsAlertAgain) { 440 setAutoDismissViews(currentNotification, alertEntry); 441 } 442 CarNotificationTypeItem notificationTypeItem = NotificationUtils.getNotificationViewType( 443 alertEntry); 444 currentNotification.setClickHandlerFactory(mClickHandlerFactory); 445 446 if (currentNotification.getNotificationView() == null) { 447 currentNotification.setNotificationView(mInflater.inflate( 448 notificationTypeItem.getHeadsUpTemplate(), 449 null)); 450 mHunContainer.displayNotification(currentNotification.getNotificationView(), 451 notificationTypeItem); 452 currentNotification.setViewHolder( 453 notificationTypeItem.getViewHolder(currentNotification.getNotificationView(), 454 mClickHandlerFactory)); 455 } 456 457 currentNotification.getViewHolder().setHideDismissButton(!isHeadsUpDismissible(alertEntry)); 458 459 if (mShouldRestrictMessagePreview && notificationTypeItem.getNotificationType() 460 == NotificationViewType.MESSAGE) { 461 ((MessageNotificationViewHolder) currentNotification.getViewHolder()).bindRestricted( 462 alertEntry, /* isInGroup= */ false, /* isHeadsUp= */ true, /* isSeen= */ false); 463 } else { 464 currentNotification.getViewHolder().bind(alertEntry, /* isInGroup= */false, 465 /* isHeadsUp= */ true, /* isSeen= */ false); 466 } 467 468 resetViewTreeListenersEntry(currentNotification); 469 470 ViewTreeObserver viewTreeObserver = 471 currentNotification.getNotificationView().getViewTreeObserver(); 472 473 // measure the size of the card and make that area of the screen touchable 474 OnComputeInternalInsetsListener onComputeInternalInsetsListener = 475 info -> setInternalInsetsInfo(info, currentNotification, 476 /* panelExpanded= */ false); 477 viewTreeObserver.addOnComputeInternalInsetsListener(onComputeInternalInsetsListener); 478 // Get the height of the notification view after onLayout() in order to animate the 479 // notification into the screen. 480 viewTreeObserver.addOnGlobalLayoutListener( 481 new OnGlobalLayoutListener() { 482 @Override 483 public void onGlobalLayout() { 484 View view = currentNotification.getNotificationView(); 485 if (shouldShowAnimation) { 486 mAnimationHelper.resetHUNPosition(view); 487 AnimatorSet animatorSet = mAnimationHelper.getAnimateInAnimator( 488 mContext, view); 489 animatorSet.setTarget(view); 490 animatorSet.start(); 491 } 492 view.getViewTreeObserver().removeOnGlobalLayoutListener(this); 493 } 494 }); 495 // Reset the auto dismiss timeout for each rotary event. 496 OnGlobalFocusChangeListener onGlobalFocusChangeListener = 497 (oldFocus, newFocus) -> setAutoDismissViews(currentNotification, alertEntry); 498 viewTreeObserver.addOnGlobalFocusChangeListener(onGlobalFocusChangeListener); 499 500 mRegisteredViewTreeListeners.put(currentNotification, 501 new Pair<>(onComputeInternalInsetsListener, onGlobalFocusChangeListener)); 502 503 attachHunViewListeners(currentNotification.getNotificationView(), alertEntry); 504 } 505 attachHunViewListeners(View notificationView, AlertEntry alertEntry)506 private void attachHunViewListeners(View notificationView, AlertEntry alertEntry) { 507 // Add swipe gesture 508 View cardView = notificationView.findViewById(R.id.card_view); 509 cardView.setOnTouchListener(new HeadsUpNotificationOnTouchListener(cardView, 510 isHeadsUpDismissible(alertEntry), 511 () -> dismissHun(alertEntry, /* shouldAnimate= */ false))); 512 513 // Add dismiss button listener 514 View dismissButton = notificationView.findViewById( 515 R.id.dismiss_button); 516 if (dismissButton != null) { 517 dismissButton.setOnClickListener(v -> 518 dismissHun(alertEntry, /* shouldAnimate= */ true)); 519 } 520 } 521 resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry)522 private void resetViewTreeListenersEntry(HeadsUpEntry headsUpEntry) { 523 Pair<OnComputeInternalInsetsListener, OnGlobalFocusChangeListener> listeners = 524 mRegisteredViewTreeListeners.get(headsUpEntry); 525 if (listeners == null) { 526 return; 527 } 528 529 ViewTreeObserver observer = headsUpEntry.getNotificationView().getViewTreeObserver(); 530 observer.removeOnComputeInternalInsetsListener(listeners.first); 531 observer.removeOnGlobalFocusChangeListener(listeners.second); 532 mRegisteredViewTreeListeners.remove(headsUpEntry); 533 } 534 setInternalInsetsInfo(InternalInsetsInfo info, HeadsUpEntry currentNotification, boolean panelExpanded)535 protected void setInternalInsetsInfo(InternalInsetsInfo info, 536 HeadsUpEntry currentNotification, boolean panelExpanded) { 537 // If the panel is not on screen don't modify the touch region 538 if (!mHunContainer.isVisible()) return; 539 int[] mTmpTwoArray = new int[2]; 540 View cardView = currentNotification.getNotificationView().findViewById( 541 R.id.card_view); 542 543 if (cardView == null) return; 544 545 if (panelExpanded) { 546 info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); 547 return; 548 } 549 550 cardView.getLocationInWindow(mTmpTwoArray); 551 int minX = mTmpTwoArray[0]; 552 int maxX = mTmpTwoArray[0] + cardView.getWidth(); 553 int minY = mTmpTwoArray[1] + mNotificationHeadsUpCardMarginTop; 554 int maxY = mTmpTwoArray[1] + mNotificationHeadsUpCardMarginTop + cardView.getHeight(); 555 info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 556 info.touchableRegion.set(minX, minY, maxX, maxY); 557 } 558 playSound(AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)559 private void playSound(AlertEntry alertEntry, 560 NotificationListenerService.RankingMap rankingMap) { 561 NotificationListenerService.Ranking ranking = getRanking(); 562 if (rankingMap.getRanking(alertEntry.getKey(), ranking)) { 563 NotificationChannel notificationChannel = ranking.getChannel(); 564 // If sound is not set on the notification channel and default is not chosen it 565 // can be null. 566 if (notificationChannel.getSound() != null) { 567 // make the sound 568 mBeeper.beep(alertEntry.getStatusBarNotification().getPackageName(), 569 notificationChannel.getSound()); 570 } 571 } 572 } 573 574 /** 575 * @return true if the {@code alertEntry} can be dismissed/swiped away. 576 */ isHeadsUpDismissible(@onNull AlertEntry alertEntry)577 public static boolean isHeadsUpDismissible(@NonNull AlertEntry alertEntry) { 578 return !(hasFullScreenIntent(alertEntry) 579 && Objects.equals(alertEntry.getNotification().category, Notification.CATEGORY_CALL) 580 && alertEntry.getStatusBarNotification().isOngoing()); 581 } 582 583 @VisibleForTesting getActiveHeadsUpNotifications()584 protected Map<String, HeadsUpEntry> getActiveHeadsUpNotifications() { 585 return mActiveHeadsUpNotifications; 586 } 587 setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry)588 private void setAutoDismissViews(HeadsUpEntry currentNotification, AlertEntry alertEntry) { 589 // Should not auto dismiss if HUN has a full screen Intent. 590 if (hasFullScreenIntent(alertEntry)) { 591 return; 592 } 593 currentNotification.getHandler().removeCallbacksAndMessages(null); 594 currentNotification.getHandler().postDelayed( 595 () -> dismissHun(alertEntry, /* shouldAnimate= */ true), mDuration); 596 } 597 598 /** 599 * Returns true if AlertEntry has a full screen Intent. 600 */ hasFullScreenIntent(@onNull AlertEntry alertEntry)601 private static boolean hasFullScreenIntent(@NonNull AlertEntry alertEntry) { 602 return alertEntry.getNotification().fullScreenIntent != null; 603 } 604 605 /** 606 * Dismisses the heads up notification and reset the views. 607 * 608 * @param shouldAnimate if set to true will animate the HUN out of the screen. 609 */ dismissHun(AlertEntry alertEntry, boolean shouldAnimate)610 private void dismissHun(AlertEntry alertEntry, boolean shouldAnimate) { 611 if (!isActiveHun(alertEntry) 612 || mDismissingHeadsUpNotifications.contains(alertEntry.getKey())) { 613 if (DEBUG) { 614 Log.d(TAG, "HUN not active, cannot dismiss, key: " + alertEntry.getKey()); 615 } 616 return; 617 } 618 mDismissingHeadsUpNotifications.add(alertEntry.getKey()); 619 620 if (DEBUG) { 621 Log.d(TAG, "Dismissing HUN, key: " + alertEntry.getKey() 622 + ", shouldAnimate: " + shouldAnimate); 623 } 624 resetHeadsUpEntry(alertEntry); 625 View headsUpView = getHeadsUpView(alertEntry); 626 627 if (headsUpView == null) { 628 return; 629 } 630 631 if (!shouldAnimate) { 632 postDismiss(alertEntry, headsUpView); 633 return; 634 } 635 636 637 AnimatorSet animatorSet = mAnimationHelper.getAnimateOutAnimator(mContext, headsUpView); 638 animatorSet.setTarget(headsUpView); 639 animatorSet.addListener(new AnimatorListenerAdapter() { 640 @Override 641 public void onAnimationEnd(Animator animation) { 642 postDismiss(alertEntry, headsUpView); 643 } 644 }); 645 animatorSet.start(); 646 647 } 648 649 /** 650 * Method to be called after HUN is dismissed. 651 */ postDismiss(AlertEntry alertEntry, View headsUpView)652 private void postDismiss(AlertEntry alertEntry, View headsUpView) { 653 removeHeadsUpEntry(alertEntry, headsUpView); 654 655 boolean isRemovedBySender = 656 mHeadsUpNotificationsToBeRemoved.contains(alertEntry.getKey()); 657 handleHeadsUpNotificationStateChanged(alertEntry, 658 isRemovedBySender ? HeadsUpState.REMOVED_BY_SENDER 659 : HeadsUpState.DISMISSED); 660 661 mHeadsUpNotificationsToBeRemoved.remove(alertEntry.getKey()); 662 mDismissingHeadsUpNotifications.remove(alertEntry.getKey()); 663 } 664 resetHeadsUpEntry(@onNull AlertEntry alertEntry)665 private void resetHeadsUpEntry(@NonNull AlertEntry alertEntry) { 666 if (!isActiveHun(alertEntry)) { 667 return; 668 } 669 HeadsUpEntry currentHeadsUpNotification = getActiveHeadsUpEntry(alertEntry); 670 currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(/* token= */ null); 671 resetViewTreeListenersEntry(currentHeadsUpNotification); 672 } 673 674 @Nullable getHeadsUpView(@onNull AlertEntry alertEntry)675 private View getHeadsUpView(@NonNull AlertEntry alertEntry) { 676 if (!isActiveHun(alertEntry)) { 677 return null; 678 } 679 return getActiveHeadsUpEntry(alertEntry).getNotificationView(); 680 } 681 removeHeadsUpEntry(@onNull AlertEntry alertEntry, @Nullable View view)682 private void removeHeadsUpEntry(@NonNull AlertEntry alertEntry, @Nullable View view) { 683 if (view != null) { 684 mHunContainer.removeNotification(view); 685 } 686 mActiveHeadsUpNotifications.remove(alertEntry.getKey()); 687 } 688 689 /** 690 * Helper method that determines whether a notification should show as a heads-up. 691 * 692 * <p> A notification will never be shown as a heads-up if: 693 * <ul> 694 * <li> Keyguard (lock screen) is showing 695 * <li> OEMs configured CATEGORY_NAVIGATION should not be shown 696 * <li> Notification is muted. 697 * </ul> 698 * 699 * <p> A notification will be shown as a heads-up if: 700 * <ul> 701 * <li> Importance >= HIGH 702 * <li> it comes from an app signed with the platform key. 703 * <li> it comes from a privileged system app. 704 * <li> is a car compatible notification. 705 * {@link com.android.car.assist.client.CarAssistUtils#isCarCompatibleMessagingNotification} 706 * <li> Notification category is one of CATEGORY_CALL or CATEGORY_NAVIGATION 707 * </ul> 708 * 709 * <p> Group alert behavior still follows API documentation. 710 * 711 * @return true if a notification should be shown as a heads-up 712 */ shouldShowHeadsUp( AlertEntry alertEntry, NotificationListenerService.RankingMap rankingMap)713 private boolean shouldShowHeadsUp( 714 AlertEntry alertEntry, 715 NotificationListenerService.RankingMap rankingMap) { 716 if (mKeyguardManager.isKeyguardLocked()) { 717 if (DEBUG) { 718 Log.d(TAG, "Unable to show as HUN: Keyguard is locked"); 719 } 720 return false; 721 } 722 Notification notification = alertEntry.getNotification(); 723 724 // Navigation notification configured by OEM 725 if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals( 726 notification.category)) { 727 if (DEBUG) { 728 Log.d(TAG, "Unable to show as HUN: OEM has disabled navigation HUN"); 729 } 730 return false; 731 } 732 // Group alert behavior 733 if (notification.suppressAlertingDueToGrouping()) { 734 if (DEBUG) { 735 Log.d(TAG, "Unable to show as HUN: Grouping notification"); 736 } 737 return false; 738 } 739 // Messaging notification muted by user. 740 if (mNotificationDataManager.isMessageNotificationMuted(alertEntry)) { 741 if (DEBUG) { 742 Log.d(TAG, "Unable to show as HUN: Messaging notification is muted by user"); 743 } 744 return false; 745 } 746 747 // Do not show if importance < HIGH 748 NotificationListenerService.Ranking ranking = getRanking(); 749 if (rankingMap.getRanking(alertEntry.getKey(), ranking)) { 750 if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) { 751 if (DEBUG) { 752 Log.d(TAG, "Unable to show as HUN: importance is not sufficient"); 753 } 754 return false; 755 } 756 } 757 758 if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, alertEntry)) { 759 if (DEBUG) { 760 Log.d(TAG, "Show as HUN: application is system privileged or signed with " 761 + "platform key"); 762 } 763 return true; 764 } 765 766 // Allow car messaging type. 767 if (isCarCompatibleMessagingNotification(alertEntry.getStatusBarNotification())) { 768 if (DEBUG) { 769 Log.d(TAG, "Show as HUN: car messaging type notification"); 770 } 771 return true; 772 } 773 774 if (notification.category == null) { 775 Log.d(TAG, "category not set for: " 776 + alertEntry.getStatusBarNotification().getPackageName()); 777 } 778 779 if (DEBUG) { 780 Log.d(TAG, "Notification category: " + notification.category); 781 } 782 783 // Allow for Call, and nav TBT categories. 784 return Notification.CATEGORY_CALL.equals(notification.category) 785 || Notification.CATEGORY_NAVIGATION.equals(notification.category); 786 } 787 isActiveHun(AlertEntry alertEntry)788 private boolean isActiveHun(AlertEntry alertEntry) { 789 return mActiveHeadsUpNotifications.containsKey(alertEntry.getKey()); 790 } 791 getActiveHeadsUpEntry(AlertEntry alertEntry)792 private HeadsUpEntry getActiveHeadsUpEntry(AlertEntry alertEntry) { 793 return mActiveHeadsUpNotifications.get(alertEntry.getKey()); 794 } 795 796 /** 797 * We tag HUN that was removed by the app and hence not to be shown in the notification panel 798 * against the normal behaviour (on dismiss add to notification panel). 799 */ tagCurrentActiveHunToBeRemoved(AlertEntry alertEntry)800 private void tagCurrentActiveHunToBeRemoved(AlertEntry alertEntry) { 801 mHeadsUpNotificationsToBeRemoved.add(alertEntry.getKey()); 802 } 803 804 @VisibleForTesting getRanking()805 protected NotificationListenerService.Ranking getRanking() { 806 return new NotificationListenerService.Ranking(); 807 } 808 809 @Override onUxRestrictionsChanged(CarUxRestrictions restrictions)810 public void onUxRestrictionsChanged(CarUxRestrictions restrictions) { 811 mCarHeadsUpNotificationQueue.setActiveUxRestriction( 812 restrictions.isRequiresDistractionOptimization()); 813 mShouldRestrictMessagePreview = 814 (restrictions.getActiveRestrictions() 815 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0; 816 } 817 818 /** 819 * Sets the source of {@link View.OnClickListener} 820 * 821 * @param clickHandlerFactory used to generate onClickListeners 822 */ 823 @VisibleForTesting setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)824 public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) { 825 mClickHandlerFactory = clickHandlerFactory; 826 } 827 828 @VisibleForTesting 829 CarHeadsUpNotificationQueue.CarHeadsUpNotificationQueueCallback getCarHeadsUpNotificationQueueCallback()830 getCarHeadsUpNotificationQueueCallback() { 831 return mCarHeadsUpNotificationQueueCallback; 832 } 833 834 @VisibleForTesting setCarHeadsUpNotificationQueue(CarHeadsUpNotificationQueue carHeadsUpNotificationQueue)835 void setCarHeadsUpNotificationQueue(CarHeadsUpNotificationQueue carHeadsUpNotificationQueue) { 836 mCarHeadsUpNotificationQueue = carHeadsUpNotificationQueue; 837 } 838 839 @VisibleForTesting addActiveHeadsUpNotification(HeadsUpEntry headsUpEntry)840 void addActiveHeadsUpNotification(HeadsUpEntry headsUpEntry) { 841 mActiveHeadsUpNotifications.put(headsUpEntry.getKey(), headsUpEntry); 842 } 843 } 844