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