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 com.android.car.assist.client.CarAssistUtils.isCarCompatibleMessagingNotification; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.app.KeyguardManager; 25 import android.app.Notification; 26 import android.app.NotificationChannel; 27 import android.app.NotificationManager; 28 import android.car.drivingstate.CarUxRestrictions; 29 import android.car.drivingstate.CarUxRestrictionsManager; 30 import android.car.userlib.CarUserManagerHelper; 31 import android.content.Context; 32 import android.graphics.PixelFormat; 33 import android.os.Bundle; 34 import android.service.notification.NotificationListenerService; 35 import android.service.notification.StatusBarNotification; 36 import android.util.Log; 37 import android.view.Gravity; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewTreeObserver; 41 import android.view.WindowManager; 42 import android.view.animation.AnimationUtils; 43 import android.view.animation.Interpolator; 44 import android.widget.FrameLayout; 45 46 import androidx.annotation.VisibleForTesting; 47 48 import com.android.car.notification.template.BasicNotificationViewHolder; 49 import com.android.car.notification.template.CallNotificationViewHolder; 50 import com.android.car.notification.template.EmergencyNotificationViewHolder; 51 import com.android.car.notification.template.InboxNotificationViewHolder; 52 import com.android.car.notification.template.MessageNotificationViewHolder; 53 import com.android.car.notification.template.NavigationNotificationViewHolder; 54 55 import java.util.HashMap; 56 import java.util.Map; 57 58 /** 59 * Notification Manager for heads-up notifications in car. 60 */ 61 public class CarHeadsUpNotificationManager 62 implements CarUxRestrictionsManager.OnUxRestrictionsChangedListener { 63 private static final String TAG = CarHeadsUpNotificationManager.class.getSimpleName(); 64 65 private final Beeper mBeeper; 66 private final Context mContext; 67 private final boolean mEnableNavigationHeadsup; 68 private final long mDuration; 69 private final long mMinDisplayDuration; 70 private final long mEnterAnimationDuration; 71 private final long mAlphaEnterAnimationDuration; 72 private final long mExitAnimationDuration; 73 private final int mNotificationHeadsUpCardMarginTop; 74 75 private final KeyguardManager mKeyguardManager; 76 private final CarUserManagerHelper mCarUserManagerHelper; 77 private final PreprocessingManager mPreprocessingManager; 78 private final WindowManager mWindowManager; 79 private final LayoutInflater mInflater; 80 81 private boolean mShouldRestrictMessagePreview; 82 private NotificationClickHandlerFactory mClickHandlerFactory; 83 private NotificationDataManager mNotificationDataManager; 84 85 // key for the map is the statusbarnotification key 86 private final Map<String, HeadsUpEntry> mActiveHeadsUpNotifications; 87 // view that contains scrim and notification content 88 protected final View mHeadsUpPanel; 89 // framelayout that notification content should be added to. 90 protected final FrameLayout mHeadsUpContentFrame; 91 CarHeadsUpNotificationManager(Context context, NotificationClickHandlerFactory clickHandlerFactory, NotificationDataManager notificationDataManager)92 public CarHeadsUpNotificationManager(Context context, 93 NotificationClickHandlerFactory clickHandlerFactory, 94 NotificationDataManager notificationDataManager) { 95 mContext = context.getApplicationContext(); 96 mEnableNavigationHeadsup = 97 context.getResources().getBoolean(R.bool.config_showNavigationHeadsup); 98 mClickHandlerFactory = clickHandlerFactory; 99 mNotificationDataManager = notificationDataManager; 100 mBeeper = new Beeper(mContext); 101 mDuration = mContext.getResources().getInteger(R.integer.headsup_notification_duration_ms); 102 mNotificationHeadsUpCardMarginTop = (int) mContext.getResources().getDimension( 103 R.dimen.headsup_notification_top_margin); 104 mMinDisplayDuration = mContext.getResources().getInteger( 105 R.integer.heads_up_notification_minimum_time); 106 mEnterAnimationDuration = 107 mContext.getResources().getInteger(R.integer.headsup_total_enter_duration_ms); 108 mAlphaEnterAnimationDuration = 109 mContext.getResources().getInteger(R.integer.headsup_alpha_enter_duration_ms); 110 mExitAnimationDuration = 111 mContext.getResources().getInteger(R.integer.headsup_exit_duration_ms); 112 mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 113 mPreprocessingManager = PreprocessingManager.getInstance(context); 114 mWindowManager = 115 (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 116 mInflater = LayoutInflater.from(mContext); 117 mActiveHeadsUpNotifications = new HashMap<>(); 118 mHeadsUpPanel = createHeadsUpPanel(); 119 mHeadsUpContentFrame = mHeadsUpPanel.findViewById(R.id.headsup_content); 120 mCarUserManagerHelper = new CarUserManagerHelper(mContext); 121 addHeadsUpPanelToDisplay(); 122 } 123 124 /** 125 * Construct and return the heads up panel. 126 * 127 * @return view that contains R.id.headsup_content 128 */ createHeadsUpPanel()129 protected View createHeadsUpPanel() { 130 return mInflater.inflate(R.layout.headsup_container, null); 131 } 132 133 /** 134 * Attach the heads up panel to the display 135 */ addHeadsUpPanelToDisplay()136 protected void addHeadsUpPanelToDisplay() { 137 WindowManager.LayoutParams wrapperParams = new WindowManager.LayoutParams( 138 WindowManager.LayoutParams.MATCH_PARENT, 139 WindowManager.LayoutParams.MATCH_PARENT, 140 // This type allows covering status bar and receiving touch input 141 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, 142 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 143 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 144 PixelFormat.TRANSLUCENT); 145 wrapperParams.gravity = Gravity.TOP; 146 mHeadsUpPanel.setVisibility(View.INVISIBLE); 147 mWindowManager.addView(mHeadsUpPanel, wrapperParams); 148 } 149 150 /** 151 * Set the Heads Up view to visible 152 */ setHeadsUpVisible()153 protected void setHeadsUpVisible() { 154 mHeadsUpPanel.setVisibility(View.VISIBLE); 155 } 156 157 /** 158 * Show the notification as a heads-up if it meets the criteria. 159 */ maybeShowHeadsUp( StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap, Map<String, StatusBarNotification> activeNotifications)160 public void maybeShowHeadsUp( 161 StatusBarNotification statusBarNotification, 162 NotificationListenerService.RankingMap rankingMap, 163 Map<String, StatusBarNotification> activeNotifications) { 164 if (!shouldShowHeadsUp(statusBarNotification, rankingMap)) { 165 // check if this is a update to the existing notification and if it should still show 166 // as a heads up or not. 167 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 168 statusBarNotification.getKey()); 169 if (currentActiveHeadsUpNotification == null) { 170 activeNotifications.put(statusBarNotification.getKey(), statusBarNotification); 171 return; 172 } 173 if (CarNotificationDiff.sameNotificationKey( 174 currentActiveHeadsUpNotification.getStatusBarNotification(), 175 statusBarNotification) 176 && currentActiveHeadsUpNotification.getHandler().hasMessagesOrCallbacks()) { 177 animateOutHUN(statusBarNotification); 178 } 179 activeNotifications.put(statusBarNotification.getKey(), statusBarNotification); 180 return; 181 } 182 if (!activeNotifications.containsKey(statusBarNotification.getKey()) || canUpdate( 183 statusBarNotification) || alertAgain(statusBarNotification.getNotification())) { 184 showHeadsUp(mPreprocessingManager.optimizeForDriving(statusBarNotification), 185 rankingMap); 186 } 187 activeNotifications.put(statusBarNotification.getKey(), statusBarNotification); 188 } 189 190 /** 191 * This method gets called when an app wants to cancel or withdraw its notification. 192 */ maybeRemoveHeadsUp(StatusBarNotification statusBarNotification)193 public void maybeRemoveHeadsUp(StatusBarNotification statusBarNotification) { 194 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 195 statusBarNotification.getKey()); 196 // if the heads up notification is already removed do nothing. 197 if (currentActiveHeadsUpNotification == null) { 198 return; 199 } 200 201 long totalDisplayDuration = 202 System.currentTimeMillis() - currentActiveHeadsUpNotification.getPostTime(); 203 // ongoing notification that has passed the minimum threshold display time. 204 if (totalDisplayDuration >= mMinDisplayDuration) { 205 animateOutHUN(statusBarNotification); 206 return; 207 } 208 209 long earliestRemovalTime = mMinDisplayDuration - totalDisplayDuration; 210 211 currentActiveHeadsUpNotification.getHandler().postDelayed(() -> 212 animateOutHUN(statusBarNotification), earliestRemovalTime); 213 } 214 215 /** 216 * Returns true if the notification's flag is not set to 217 * {@link Notification#FLAG_ONLY_ALERT_ONCE} 218 */ alertAgain(Notification newNotification)219 private boolean alertAgain(Notification newNotification) { 220 return (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; 221 } 222 223 /** 224 * Return true if the currently displaying notification have the same key as the new added 225 * notification. In that case it will be considered as an update to the currently displayed 226 * notification. 227 */ isUpdate(StatusBarNotification statusBarNotification)228 private boolean isUpdate(StatusBarNotification statusBarNotification) { 229 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 230 statusBarNotification.getKey()); 231 if (currentActiveHeadsUpNotification == null) { 232 return false; 233 } 234 return CarNotificationDiff.sameNotificationKey( 235 currentActiveHeadsUpNotification.getStatusBarNotification(), 236 statusBarNotification); 237 } 238 239 /** 240 * Updates only when the notification is being displayed. 241 */ canUpdate(StatusBarNotification statusBarNotification)242 private boolean canUpdate(StatusBarNotification statusBarNotification) { 243 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 244 statusBarNotification.getKey()); 245 return currentActiveHeadsUpNotification != null && System.currentTimeMillis() - 246 currentActiveHeadsUpNotification.getPostTime() < mDuration; 247 } 248 249 /** 250 * Returns the active headsUpEntry or creates a new one while adding it to the list of 251 * mActiveHeadsUpNotifications. 252 */ addNewHeadsUpEntry(StatusBarNotification statusBarNotification)253 private HeadsUpEntry addNewHeadsUpEntry(StatusBarNotification statusBarNotification) { 254 HeadsUpEntry currentActiveHeadsUpNotification = mActiveHeadsUpNotifications.get( 255 statusBarNotification.getKey()); 256 if (currentActiveHeadsUpNotification == null) { 257 currentActiveHeadsUpNotification = new HeadsUpEntry(statusBarNotification); 258 mActiveHeadsUpNotifications.put(statusBarNotification.getKey(), 259 currentActiveHeadsUpNotification); 260 currentActiveHeadsUpNotification.isAlertAgain = alertAgain( 261 statusBarNotification.getNotification()); 262 currentActiveHeadsUpNotification.isNewHeadsUp = true; 263 return currentActiveHeadsUpNotification; 264 } 265 currentActiveHeadsUpNotification.isNewHeadsUp = false; 266 currentActiveHeadsUpNotification.isAlertAgain = alertAgain( 267 statusBarNotification.getNotification()); 268 if (currentActiveHeadsUpNotification.isAlertAgain) { 269 // This is a ongoing notification which needs to be alerted again to the user. This 270 // requires for the post time to be updated. 271 currentActiveHeadsUpNotification.updatePostTime(); 272 } 273 return currentActiveHeadsUpNotification; 274 } 275 276 /** 277 * Controls three major conditions while showing heads up notification. 278 * <p> 279 * <ol> 280 * <li> When a new HUN comes in it will be displayed with animations 281 * <li> If an update to existing HUN comes in which enforces to alert the HUN again to user, 282 * then the post time will be updated to current time. This will only be done if {@link 283 * Notification#FLAG_ONLY_ALERT_ONCE} flag is not set. 284 * <li> If an update to existing HUN comes in which just updates the data and does not want to 285 * alert itself again, then the animations will not be shown and the data will get updated. This 286 * will only be done if {@link Notification#FLAG_ONLY_ALERT_ONCE} flag is not set. 287 * </ol> 288 */ showHeadsUp(StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap)289 private void showHeadsUp(StatusBarNotification statusBarNotification, 290 NotificationListenerService.RankingMap rankingMap) { 291 // Show animations only when there is no active HUN and notification is new. This check 292 // needs to be done here because after this the new notification will be added to the map 293 // holding ongoing notifications. 294 boolean shouldShowAnimation = !isUpdate(statusBarNotification); 295 HeadsUpEntry currentNotification = addNewHeadsUpEntry(statusBarNotification); 296 if (currentNotification.isNewHeadsUp) { 297 playSound(statusBarNotification, rankingMap); 298 setHeadsUpVisible(); 299 setAutoDismissViews(currentNotification, statusBarNotification); 300 } else if (currentNotification.isAlertAgain) { 301 setAutoDismissViews(currentNotification, statusBarNotification); 302 } 303 @NotificationViewType int viewType = getNotificationViewType(statusBarNotification); 304 mClickHandlerFactory.setHeadsUpNotificationCallBack( 305 () -> animateOutHUN(statusBarNotification)); 306 currentNotification.setClickHandlerFactory(mClickHandlerFactory); 307 switch (viewType) { 308 case NotificationViewType.CAR_EMERGENCY_HEADSUP: { 309 if (currentNotification.getNotificationView() == null) { 310 currentNotification.setNotificationView(mInflater.inflate( 311 R.layout.car_emergency_headsup_notification_template, 312 null)); 313 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 314 currentNotification.setViewHolder( 315 new EmergencyNotificationViewHolder( 316 currentNotification.getNotificationView(), 317 mClickHandlerFactory)); 318 } 319 currentNotification.getViewHolder().bind(statusBarNotification, 320 /* isInGroup= */ false, /* isHeadsUp= */ true); 321 break; 322 } 323 case NotificationViewType.NAVIGATION: { 324 if (currentNotification.getNotificationView() == null) { 325 currentNotification.setNotificationView(mInflater.inflate( 326 R.layout.navigation_headsup_notification_template, 327 null)); 328 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 329 currentNotification.setViewHolder( 330 new NavigationNotificationViewHolder( 331 currentNotification.getNotificationView(), 332 mClickHandlerFactory)); 333 } 334 currentNotification.getViewHolder().bind(statusBarNotification, 335 /* isInGroup= */ false, /* isHeadsUp= */ true); 336 break; 337 } 338 case NotificationViewType.CALL: { 339 if (currentNotification.getNotificationView() == null) { 340 currentNotification.setNotificationView(mInflater.inflate( 341 R.layout.call_headsup_notification_template, 342 null)); 343 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 344 currentNotification.setViewHolder( 345 new CallNotificationViewHolder( 346 currentNotification.getNotificationView(), 347 mClickHandlerFactory)); 348 } 349 currentNotification.getViewHolder().bind(statusBarNotification, 350 /* isInGroup= */ false, /* isHeadsUp= */ true); 351 break; 352 } 353 case NotificationViewType.CAR_WARNING_HEADSUP: { 354 if (currentNotification.getNotificationView() == null) { 355 currentNotification.setNotificationView(mInflater.inflate( 356 R.layout.car_warning_headsup_notification_template, 357 null)); 358 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 359 // Using the basic view holder because they share the same view binding logic 360 // OEMs should create view holders if needed 361 currentNotification.setViewHolder( 362 new BasicNotificationViewHolder( 363 currentNotification.getNotificationView(), 364 mClickHandlerFactory)); 365 } 366 currentNotification.getViewHolder().bind(statusBarNotification, /* isInGroup= */ 367 false, /* isHeadsUp= */ true); 368 break; 369 } 370 case NotificationViewType.CAR_INFORMATION_HEADSUP: { 371 if (currentNotification.getNotificationView() == null) { 372 currentNotification.setNotificationView(mInflater.inflate( 373 R.layout.car_information_headsup_notification_template, 374 null)); 375 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 376 // Using the basic view holder because they share the same view binding logic 377 // OEMs should create view holders if needed 378 currentNotification.setViewHolder( 379 new BasicNotificationViewHolder( 380 currentNotification.getNotificationView(), 381 mClickHandlerFactory)); 382 } 383 currentNotification.getViewHolder().bind(statusBarNotification, 384 /* isInGroup= */ false, /* isHeadsUp= */ true); 385 break; 386 } 387 case NotificationViewType.MESSAGE_HEADSUP: { 388 if (currentNotification.getNotificationView() == null) { 389 currentNotification.setNotificationView(mInflater.inflate( 390 R.layout.message_headsup_notification_template, 391 null)); 392 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 393 currentNotification.setViewHolder( 394 new MessageNotificationViewHolder( 395 currentNotification.getNotificationView(), 396 mClickHandlerFactory)); 397 } 398 if (mShouldRestrictMessagePreview) { 399 ((MessageNotificationViewHolder) currentNotification.getViewHolder()) 400 .bindRestricted(statusBarNotification, /* isInGroup= */ 401 false, /* isHeadsUp= */ true); 402 } else { 403 currentNotification.getViewHolder().bind(statusBarNotification, /* isInGroup= */ 404 false, /* isHeadsUp= */ true); 405 } 406 break; 407 } 408 case NotificationViewType.INBOX_HEADSUP: { 409 if (currentNotification.getNotificationView() == null) { 410 currentNotification.setNotificationView(mInflater.inflate( 411 R.layout.inbox_headsup_notification_template, 412 null)); 413 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 414 currentNotification.setViewHolder( 415 new InboxNotificationViewHolder( 416 currentNotification.getNotificationView(), 417 mClickHandlerFactory)); 418 } 419 currentNotification.getViewHolder().bind(statusBarNotification, 420 /* isInGroup= */ false, /* isHeadsUp= */ true); 421 break; 422 } 423 case NotificationViewType.BASIC_HEADSUP: 424 default: { 425 if (currentNotification.getNotificationView() == null) { 426 currentNotification.setNotificationView(mInflater.inflate( 427 R.layout.basic_headsup_notification_template, 428 null)); 429 mHeadsUpContentFrame.addView(currentNotification.getNotificationView()); 430 currentNotification.setViewHolder( 431 new BasicNotificationViewHolder( 432 currentNotification.getNotificationView(), 433 mClickHandlerFactory)); 434 } 435 currentNotification.getViewHolder().bind(statusBarNotification, 436 /* isInGroup= */ false, /* isHeadsUp= */ true); 437 break; 438 } 439 } 440 441 // measure the size of the card and make that area of the screen touchable 442 currentNotification.getNotificationView().getViewTreeObserver() 443 .addOnComputeInternalInsetsListener( 444 info -> setInternalInsetsInfo(info, 445 currentNotification, /* panelExpanded= */false)); 446 // Get the height of the notification view after onLayout() 447 // in order animate the notification in 448 currentNotification.getNotificationView().getViewTreeObserver().addOnGlobalLayoutListener( 449 new ViewTreeObserver.OnGlobalLayoutListener() { 450 @Override 451 public void onGlobalLayout() { 452 int notificationHeight = 453 currentNotification.getNotificationView().getHeight(); 454 455 if (shouldShowAnimation) { 456 currentNotification.getNotificationView().setY(0 - notificationHeight); 457 currentNotification.getNotificationView().setAlpha(0f); 458 459 Interpolator yPositionInterpolator = AnimationUtils.loadInterpolator( 460 mContext, 461 R.interpolator.heads_up_entry_direction_interpolator); 462 Interpolator alphaInterpolator = AnimationUtils.loadInterpolator( 463 mContext, 464 R.interpolator.heads_up_entry_alpha_interpolator); 465 466 ObjectAnimator moveY = ObjectAnimator.ofFloat( 467 currentNotification.getNotificationView(), "y", 0f); 468 moveY.setDuration(mEnterAnimationDuration); 469 moveY.setInterpolator(yPositionInterpolator); 470 471 ObjectAnimator alpha = ObjectAnimator.ofFloat( 472 currentNotification.getNotificationView(), "alpha", 1f); 473 alpha.setDuration(mAlphaEnterAnimationDuration); 474 alpha.setInterpolator(alphaInterpolator); 475 476 AnimatorSet animatorSet = new AnimatorSet(); 477 animatorSet.playTogether(moveY, alpha); 478 animatorSet.start(); 479 480 } 481 currentNotification.getNotificationView().getViewTreeObserver() 482 .removeOnGlobalLayoutListener(this); 483 } 484 }); 485 486 if (currentNotification.isNewHeadsUp) { 487 boolean shouldDismissOnSwipe = true; 488 if (shouldDismissOnSwipe(statusBarNotification)) { 489 shouldDismissOnSwipe = false; 490 } 491 // Add swipe gesture 492 View cardView = currentNotification.getNotificationView().findViewById(R.id.card_view); 493 cardView.setOnTouchListener( 494 new HeadsUpNotificationOnTouchListener(cardView, shouldDismissOnSwipe, 495 () -> resetView(statusBarNotification))); 496 } 497 } 498 setInternalInsetsInfo(ViewTreeObserver.InternalInsetsInfo info, HeadsUpEntry currentNotification, boolean panelExpanded)499 protected void setInternalInsetsInfo(ViewTreeObserver.InternalInsetsInfo info, 500 HeadsUpEntry currentNotification, boolean panelExpanded) { 501 // If the panel is not on screen don't modify the touch region 502 if (mHeadsUpPanel.getVisibility() != View.VISIBLE) return; 503 int[] mTmpTwoArray = new int[2]; 504 View cardView = currentNotification.getNotificationView().findViewById( 505 R.id.card_view); 506 507 if (cardView == null) return; 508 509 if (panelExpanded) { 510 info.setTouchableInsets( 511 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); 512 return; 513 } 514 515 cardView.getLocationOnScreen(mTmpTwoArray); 516 int minX = mTmpTwoArray[0]; 517 int maxX = mTmpTwoArray[0] + cardView.getWidth(); 518 int height = cardView.getHeight(); 519 info.setTouchableInsets( 520 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 521 info.touchableRegion.set(minX, mNotificationHeadsUpCardMarginTop, maxX, 522 height + mNotificationHeadsUpCardMarginTop); 523 } 524 playSound(StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap)525 private void playSound(StatusBarNotification statusBarNotification, 526 NotificationListenerService.RankingMap rankingMap) { 527 NotificationListenerService.Ranking ranking = getRanking(); 528 if (rankingMap.getRanking(statusBarNotification.getKey(), ranking)) { 529 NotificationChannel notificationChannel = ranking.getChannel(); 530 // If sound is not set on the notification channel and default is not chosen it 531 // can be null. 532 if (notificationChannel.getSound() != null) { 533 // make the sound 534 mBeeper.beep(statusBarNotification.getPackageName(), 535 notificationChannel.getSound()); 536 } 537 } 538 } 539 shouldDismissOnSwipe(StatusBarNotification statusBarNotification)540 private boolean shouldDismissOnSwipe(StatusBarNotification statusBarNotification) { 541 return hasFullScreenIntent(statusBarNotification) 542 && statusBarNotification.getNotification().category.equals( 543 Notification.CATEGORY_CALL) && statusBarNotification.isOngoing(); 544 } 545 546 547 @VisibleForTesting getActiveHeadsUpNotifications()548 protected Map<String, HeadsUpEntry> getActiveHeadsUpNotifications() { 549 return mActiveHeadsUpNotifications; 550 } 551 setAutoDismissViews(HeadsUpEntry currentNotification, StatusBarNotification statusBarNotification)552 private void setAutoDismissViews(HeadsUpEntry currentNotification, 553 StatusBarNotification statusBarNotification) { 554 // Should not auto dismiss if HUN has a full screen Intent. 555 if (hasFullScreenIntent(statusBarNotification)) { 556 return; 557 } 558 currentNotification.getHandler().removeCallbacksAndMessages(null); 559 currentNotification.getHandler().postDelayed(() -> animateOutHUN(statusBarNotification), 560 mDuration); 561 } 562 563 /** 564 * Returns true if StatusBarNotification has a full screen Intent. 565 */ hasFullScreenIntent(StatusBarNotification sbn)566 private boolean hasFullScreenIntent(StatusBarNotification sbn) { 567 return sbn.getNotification().fullScreenIntent != null; 568 } 569 570 /** 571 * Animates the heads up notification out of the screen and reset the views. 572 */ animateOutHUN(StatusBarNotification statusBarNotification)573 private void animateOutHUN(StatusBarNotification statusBarNotification) { 574 Log.d(TAG, "clearViews for Heads Up Notification: "); 575 // get the current notification to perform animations and remove it immediately from the 576 // active notification maps and cancel all other call backs if any. 577 HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get( 578 statusBarNotification.getKey()); 579 // view can also be removed when swipped away. 580 if (currentHeadsUpNotification == null) { 581 return; 582 } 583 currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null); 584 currentHeadsUpNotification.getClickHandlerFactory().setHeadsUpNotificationCallBack(null); 585 586 Interpolator exitInterpolator = AnimationUtils.loadInterpolator(mContext, 587 R.interpolator.heads_up_exit_direction_interpolator); 588 Interpolator alphaInterpolator = AnimationUtils.loadInterpolator(mContext, 589 R.interpolator.heads_up_exit_alpha_interpolator); 590 591 ObjectAnimator moveY = ObjectAnimator.ofFloat( 592 currentHeadsUpNotification.getNotificationView(), "y", 593 -1 * currentHeadsUpNotification.getNotificationView().getHeight()); 594 moveY.setDuration(mExitAnimationDuration); 595 moveY.setInterpolator(exitInterpolator); 596 597 ObjectAnimator alpha = ObjectAnimator.ofFloat( 598 currentHeadsUpNotification.getNotificationView(), "alpha", 1f); 599 alpha.setDuration(mExitAnimationDuration); 600 alpha.setInterpolator(alphaInterpolator); 601 602 AnimatorSet animatorSet = new AnimatorSet(); 603 animatorSet.playTogether(moveY, alpha); 604 animatorSet.addListener(new AnimatorListenerAdapter() { 605 @Override 606 public void onAnimationEnd(Animator animation) { 607 removeNotificationFromPanel(currentHeadsUpNotification); 608 609 // Remove HUN after the animation ends to prevent accidental touch on the card 610 // triggering another remove call. 611 mActiveHeadsUpNotifications.remove(statusBarNotification.getKey()); 612 } 613 }); 614 animatorSet.start(); 615 } 616 617 /** 618 * Remove notification from the screen. If it was the last notification hide the heads up panel. 619 * 620 * @param currentHeadsUpNotification The notification to remove 621 */ removeNotificationFromPanel(HeadsUpEntry currentHeadsUpNotification)622 protected void removeNotificationFromPanel(HeadsUpEntry currentHeadsUpNotification) { 623 mHeadsUpContentFrame.removeView(currentHeadsUpNotification.getNotificationView()); 624 if (mHeadsUpContentFrame.getChildCount() == 0) { 625 mHeadsUpPanel.setVisibility(View.INVISIBLE); 626 } 627 } 628 629 630 /** 631 * Removes the view for the active heads up notification and also removes the HUN from the map 632 * of active Notifications. 633 */ resetView(StatusBarNotification statusBarNotification)634 private void resetView(StatusBarNotification statusBarNotification) { 635 HeadsUpEntry currentHeadsUpNotification = mActiveHeadsUpNotifications.get( 636 statusBarNotification.getKey()); 637 if (currentHeadsUpNotification == null) return; 638 639 currentHeadsUpNotification.getClickHandlerFactory().setHeadsUpNotificationCallBack(null); 640 currentHeadsUpNotification.getHandler().removeCallbacksAndMessages(null); 641 removeNotificationFromPanel(currentHeadsUpNotification); 642 mActiveHeadsUpNotifications.remove(statusBarNotification.getKey()); 643 } 644 645 /** 646 * Choose a correct notification layout for this heads-up notification. 647 * Note that the layout chosen can be different for the same notification 648 * in the notification center. 649 */ 650 @NotificationViewType getNotificationViewType(StatusBarNotification statusBarNotification)651 private static int getNotificationViewType(StatusBarNotification statusBarNotification) { 652 String category = statusBarNotification.getNotification().category; 653 if (category != null) { 654 switch (category) { 655 case Notification.CATEGORY_CAR_EMERGENCY: 656 return NotificationViewType.CAR_EMERGENCY_HEADSUP; 657 case Notification.CATEGORY_NAVIGATION: 658 return NotificationViewType.NAVIGATION; 659 case Notification.CATEGORY_CALL: 660 return NotificationViewType.CALL; 661 case Notification.CATEGORY_CAR_WARNING: 662 return NotificationViewType.CAR_WARNING_HEADSUP; 663 case Notification.CATEGORY_CAR_INFORMATION: 664 return NotificationViewType.CAR_INFORMATION_HEADSUP; 665 case Notification.CATEGORY_MESSAGE: 666 return NotificationViewType.MESSAGE_HEADSUP; 667 default: 668 break; 669 } 670 } 671 Bundle extras = statusBarNotification.getNotification().extras; 672 if (extras.containsKey(Notification.EXTRA_BIG_TEXT) 673 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT)) { 674 return NotificationViewType.INBOX_HEADSUP; 675 } 676 // progress, media, big text, big picture, and basic templates 677 return NotificationViewType.BASIC_HEADSUP; 678 } 679 680 /** 681 * Helper method that determines whether a notification should show as a heads-up. 682 * 683 * <p> A notification will never be shown as a heads-up if: 684 * <ul> 685 * <li> Keyguard (lock screen) is showing 686 * <li> OEMs configured CATEGORY_NAVIGATION should not be shown 687 * <li> Notification is muted. 688 * </ul> 689 * 690 * <p> A notification will be shown as a heads-up if: 691 * <ul> 692 * <li> Importance >= HIGH 693 * <li> it comes from an app signed with the platform key. 694 * <li> it comes from a privileged system app. 695 * <li> is a car compatible notification. 696 * {@link com.android.car.assist.client.CarAssistUtils#isCarCompatibleMessagingNotification} 697 * <li> Notification category is one of CATEGORY_CALL or CATEGORY_NAVIGATION 698 * </ul> 699 * 700 * <p> Group alert behavior still follows API documentation. 701 * 702 * @return true if a notification should be shown as a heads-up 703 */ shouldShowHeadsUp( StatusBarNotification statusBarNotification, NotificationListenerService.RankingMap rankingMap)704 private boolean shouldShowHeadsUp( 705 StatusBarNotification statusBarNotification, 706 NotificationListenerService.RankingMap rankingMap) { 707 if (mKeyguardManager.isKeyguardLocked()) { 708 return false; 709 } 710 Notification notification = statusBarNotification.getNotification(); 711 712 // Navigation notification configured by OEM 713 if (!mEnableNavigationHeadsup && Notification.CATEGORY_NAVIGATION.equals( 714 notification.category)) { 715 return false; 716 } 717 // Group alert behavior 718 if (notification.suppressAlertingDueToGrouping()) { 719 return false; 720 } 721 // Messaging notification muted by user. 722 if (mNotificationDataManager.isMessageNotificationMuted(statusBarNotification)) { 723 return false; 724 } 725 726 // Do not show if importance < HIGH 727 NotificationListenerService.Ranking ranking = getRanking(); 728 if (rankingMap.getRanking(statusBarNotification.getKey(), ranking)) { 729 if (ranking.getImportance() < NotificationManager.IMPORTANCE_HIGH) { 730 return false; 731 } 732 } 733 734 if (NotificationUtils.isSystemPrivilegedOrPlatformKey(mContext, 735 statusBarNotification)) { 736 return true; 737 } 738 739 // Allow car messaging type. 740 if (isCarCompatibleMessagingNotification(statusBarNotification)) { 741 return true; 742 } 743 744 if (notification.category == null) { 745 Log.d(TAG, "category not set for: " + statusBarNotification.getPackageName()); 746 } 747 748 // Allow for Call, and nav TBT categories. 749 if (Notification.CATEGORY_CALL.equals(notification.category) 750 || Notification.CATEGORY_NAVIGATION.equals(notification.category)) { 751 return true; 752 } 753 return false; 754 } 755 756 @VisibleForTesting getRanking()757 protected NotificationListenerService.Ranking getRanking() { 758 return new NotificationListenerService.Ranking(); 759 } 760 761 @Override onUxRestrictionsChanged(CarUxRestrictions restrictions)762 public void onUxRestrictionsChanged(CarUxRestrictions restrictions) { 763 mShouldRestrictMessagePreview = 764 (restrictions.getActiveRestrictions() 765 & CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE) != 0; 766 } 767 768 /** 769 * Sets the source of {@link View.OnClickListener} 770 * 771 * @param clickHandlerFactory used to generate onClickListeners 772 */ 773 @VisibleForTesting setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory)774 public void setClickHandlerFactory(NotificationClickHandlerFactory clickHandlerFactory) { 775 mClickHandlerFactory = clickHandlerFactory; 776 } 777 778 /** 779 * Callback that will be issued after a heads up notification is clicked 780 */ 781 public interface Callback { 782 /** 783 * Clears Heads up notification on click. 784 */ clearHeadsUpNotification()785 void clearHeadsUpNotification(); 786 } 787 } 788