1 /* 2 * Copyright (C) 2020 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 17 package com.android.systemui.car.notification; 18 19 import android.app.ActivityManager; 20 import android.car.Car; 21 import android.car.drivingstate.CarUxRestrictionsManager; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Rect; 25 import android.graphics.drawable.Drawable; 26 import android.inputmethodservice.InputMethodService; 27 import android.os.IBinder; 28 import android.os.RemoteException; 29 import android.util.Log; 30 import android.view.GestureDetector; 31 import android.view.KeyEvent; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.WindowInsets; 36 37 import androidx.annotation.NonNull; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import com.android.car.notification.CarNotificationListener; 41 import com.android.car.notification.CarNotificationView; 42 import com.android.car.notification.CarUxRestrictionManagerWrapper; 43 import com.android.car.notification.NotificationClickHandlerFactory; 44 import com.android.car.notification.NotificationClickHandlerFactory.OnNotificationClickListener; 45 import com.android.car.notification.NotificationDataManager; 46 import com.android.car.notification.NotificationViewController; 47 import com.android.car.notification.PreprocessingManager; 48 import com.android.internal.statusbar.IStatusBarService; 49 import com.android.systemui.R; 50 import com.android.systemui.car.CarDeviceProvisionedController; 51 import com.android.systemui.car.CarServiceProvider; 52 import com.android.systemui.car.CarServiceProvider.CarServiceOnConnectedListener; 53 import com.android.systemui.car.window.OverlayPanelViewController; 54 import com.android.systemui.car.window.OverlayViewController; 55 import com.android.systemui.car.window.OverlayViewGlobalStateController; 56 import com.android.systemui.dagger.SysUISingleton; 57 import com.android.systemui.dagger.qualifiers.Main; 58 import com.android.systemui.dagger.qualifiers.UiBackground; 59 import com.android.systemui.plugins.statusbar.StatusBarStateController; 60 import com.android.systemui.statusbar.CommandQueue; 61 import com.android.systemui.statusbar.StatusBarState; 62 import com.android.wm.shell.animation.FlingAnimationUtils; 63 64 import java.util.concurrent.Executor; 65 66 import javax.inject.Inject; 67 68 /** View controller for the notification panel. */ 69 @SysUISingleton 70 public class NotificationPanelViewController extends OverlayPanelViewController 71 implements CommandQueue.Callbacks { 72 73 private static final boolean DEBUG = true; 74 private static final String TAG = "NotificationPanelViewController"; 75 76 private final Context mContext; 77 private final Resources mResources; 78 private final CarServiceProvider mCarServiceProvider; 79 private final IStatusBarService mBarService; 80 private final CommandQueue mCommandQueue; 81 private final Executor mUiBgExecutor; 82 private final NotificationDataManager mNotificationDataManager; 83 private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper; 84 private final CarNotificationListener mCarNotificationListener; 85 private final NotificationClickHandlerFactory mNotificationClickHandlerFactory; 86 private final StatusBarStateController mStatusBarStateController; 87 private final boolean mEnableHeadsUpNotificationWhenNotificationPanelOpen; 88 private final NotificationVisibilityLogger mNotificationVisibilityLogger; 89 90 private final boolean mFitTopSystemBarInset; 91 private final boolean mFitBottomSystemBarInset; 92 private final boolean mFitLeftSystemBarInset; 93 private final boolean mFitRightSystemBarInset; 94 95 private float mInitialBackgroundAlpha; 96 private float mBackgroundAlphaDiff; 97 98 private CarNotificationView mNotificationView; 99 private RecyclerView mNotificationList; 100 private NotificationViewController mNotificationViewController; 101 102 private boolean mNotificationListAtEnd; 103 private float mFirstTouchDownOnGlassPane; 104 private boolean mNotificationListAtEndAtTimeOfTouch; 105 private boolean mIsSwipingVerticallyToClose; 106 private boolean mIsNotificationCardSwiping; 107 private boolean mImeVisible = false; 108 private boolean mOnConnectListenerAdded; 109 110 private OnUnseenCountUpdateListener mUnseenCountUpdateListener; 111 private OnNotificationClickListener mOnNotificationClickListener = 112 (launchResult, alertEntry) -> { 113 if (launchResult == ActivityManager.START_TASK_TO_FRONT 114 || launchResult == ActivityManager.START_SUCCESS 115 || launchResult == ActivityManager.START_DELIVERED_TO_TOP) { 116 animateCollapsePanel(); 117 } 118 }; 119 120 private CarServiceOnConnectedListener mCarConnectedListener = 121 new CarServiceOnConnectedListener() { 122 @Override 123 public void onConnected(Car car) { 124 CarUxRestrictionsManager carUxRestrictionsManager = 125 (CarUxRestrictionsManager) 126 car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE); 127 mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager( 128 carUxRestrictionsManager); 129 130 PreprocessingManager preprocessingManager = 131 PreprocessingManager.getInstance(mContext); 132 preprocessingManager.setCarUxRestrictionManagerWrapper( 133 mCarUxRestrictionManagerWrapper); 134 135 mNotificationViewController.enable(); 136 } 137 }; 138 139 @Inject NotificationPanelViewController( Context context, @Main Resources resources, OverlayViewGlobalStateController overlayViewGlobalStateController, FlingAnimationUtils.Builder flingAnimationUtilsBuilder, @UiBackground Executor uiBgExecutor, CarServiceProvider carServiceProvider, CarDeviceProvisionedController carDeviceProvisionedController, IStatusBarService barService, CommandQueue commandQueue, NotificationDataManager notificationDataManager, CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, CarNotificationListener carNotificationListener, NotificationClickHandlerFactory notificationClickHandlerFactory, NotificationVisibilityLogger notificationVisibilityLogger, StatusBarStateController statusBarStateController )140 public NotificationPanelViewController( 141 Context context, 142 @Main Resources resources, 143 OverlayViewGlobalStateController overlayViewGlobalStateController, 144 FlingAnimationUtils.Builder flingAnimationUtilsBuilder, 145 @UiBackground Executor uiBgExecutor, 146 147 /* Other things */ 148 CarServiceProvider carServiceProvider, 149 CarDeviceProvisionedController carDeviceProvisionedController, 150 151 /* Things needed for notifications */ 152 IStatusBarService barService, 153 CommandQueue commandQueue, 154 NotificationDataManager notificationDataManager, 155 CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper, 156 CarNotificationListener carNotificationListener, 157 NotificationClickHandlerFactory notificationClickHandlerFactory, 158 NotificationVisibilityLogger notificationVisibilityLogger, 159 160 /* Things that need to be replaced */ 161 StatusBarStateController statusBarStateController 162 ) { 163 super(context, resources, R.id.notification_panel_stub, overlayViewGlobalStateController, 164 flingAnimationUtilsBuilder, carDeviceProvisionedController); 165 mContext = context; 166 mResources = resources; 167 mCarServiceProvider = carServiceProvider; 168 mBarService = barService; 169 mCommandQueue = commandQueue; 170 mUiBgExecutor = uiBgExecutor; 171 mNotificationDataManager = notificationDataManager; 172 mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper; 173 mCarNotificationListener = carNotificationListener; 174 mNotificationClickHandlerFactory = notificationClickHandlerFactory; 175 mStatusBarStateController = statusBarStateController; 176 mNotificationVisibilityLogger = notificationVisibilityLogger; 177 178 mCommandQueue.addCallback(this); 179 180 // Notification background setup. 181 mInitialBackgroundAlpha = (float) mResources.getInteger( 182 R.integer.config_initialNotificationBackgroundAlpha) / 100; 183 if (mInitialBackgroundAlpha < 0 || mInitialBackgroundAlpha > 100) { 184 throw new RuntimeException( 185 "Unable to setup notification bar due to incorrect initial background alpha" 186 + " percentage"); 187 } 188 float finalBackgroundAlpha = Math.max( 189 mInitialBackgroundAlpha, 190 (float) mResources.getInteger( 191 R.integer.config_finalNotificationBackgroundAlpha) / 100); 192 if (finalBackgroundAlpha < 0 || finalBackgroundAlpha > 100) { 193 throw new RuntimeException( 194 "Unable to setup notification bar due to incorrect final background alpha" 195 + " percentage"); 196 } 197 mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha; 198 199 mEnableHeadsUpNotificationWhenNotificationPanelOpen = mResources.getBoolean( 200 com.android.car.notification.R.bool 201 .config_enableHeadsUpNotificationWhenNotificationPanelOpen); 202 203 mFitTopSystemBarInset = mResources.getBoolean( 204 R.bool.config_notif_panel_inset_by_top_systembar); 205 mFitBottomSystemBarInset = mResources.getBoolean( 206 R.bool.config_notif_panel_inset_by_bottom_systembar); 207 mFitLeftSystemBarInset = mResources.getBoolean( 208 R.bool.config_notif_panel_inset_by_left_systembar); 209 mFitRightSystemBarInset = mResources.getBoolean( 210 R.bool.config_notif_panel_inset_by_right_systembar); 211 212 // Inflate view on instantiation to properly initialize listeners even if panel has 213 // not been opened. 214 getOverlayViewGlobalStateController().inflateView(this); 215 } 216 217 // CommandQueue.Callbacks 218 219 @Override animateExpandNotificationsPanel()220 public void animateExpandNotificationsPanel() { 221 if (!isPanelExpanded()) { 222 toggle(); 223 } 224 } 225 226 @Override animateCollapsePanels(int flags, boolean force)227 public void animateCollapsePanels(int flags, boolean force) { 228 if (isPanelExpanded()) { 229 toggle(); 230 } 231 } 232 233 @Override setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition, boolean showImeSwitcher)234 public void setImeWindowStatus(int displayId, IBinder token, int vis, int backDisposition, 235 boolean showImeSwitcher) { 236 if (mContext.getDisplayId() != displayId) { 237 return; 238 } 239 mImeVisible = (vis & InputMethodService.IME_VISIBLE) != 0; 240 } 241 242 // OverlayViewController 243 244 @Override onFinishInflate()245 protected void onFinishInflate() { 246 reinflate(); 247 } 248 249 @Override hideInternal()250 protected void hideInternal() { 251 super.hideInternal(); 252 mNotificationVisibilityLogger.stop(); 253 } 254 255 @Override getFocusAreaViewId()256 protected int getFocusAreaViewId() { 257 return R.id.notification_container; 258 } 259 260 @Override shouldShowNavigationBarInsets()261 protected boolean shouldShowNavigationBarInsets() { 262 return true; 263 } 264 265 @Override shouldShowStatusBarInsets()266 protected boolean shouldShowStatusBarInsets() { 267 return true; 268 } 269 270 @Override getInsetSidesToFit()271 protected int getInsetSidesToFit() { 272 int insetSidesToFit = OverlayViewController.NO_INSET_SIDE; 273 274 if (mFitTopSystemBarInset) { 275 insetSidesToFit = insetSidesToFit | WindowInsets.Side.TOP; 276 } 277 278 if (mFitBottomSystemBarInset) { 279 insetSidesToFit = insetSidesToFit | WindowInsets.Side.BOTTOM; 280 } 281 282 if (mFitLeftSystemBarInset) { 283 insetSidesToFit = insetSidesToFit | WindowInsets.Side.LEFT; 284 } 285 286 if (mFitRightSystemBarInset) { 287 insetSidesToFit = insetSidesToFit | WindowInsets.Side.RIGHT; 288 } 289 290 return insetSidesToFit; 291 } 292 293 @Override shouldShowHUN()294 protected boolean shouldShowHUN() { 295 return mEnableHeadsUpNotificationWhenNotificationPanelOpen; 296 } 297 298 @Override shouldUseStableInsets()299 protected boolean shouldUseStableInsets() { 300 // When IME is visible, then the inset from the nav bar should not be applied. 301 return !mImeVisible; 302 } 303 304 /** Reinflates the view. */ reinflate()305 public void reinflate() { 306 // Do not reinflate the view if it has not been inflated at all. 307 if (!isInflated()) return; 308 309 mNotificationClickHandlerFactory.unregisterClickListener(mOnNotificationClickListener); 310 311 if (mOnConnectListenerAdded) { 312 mCarServiceProvider.removeListener(mCarConnectedListener); 313 mOnConnectListenerAdded = false; 314 } 315 316 ViewGroup container = (ViewGroup) getLayout(); 317 container.removeView(mNotificationView); 318 319 mNotificationView = (CarNotificationView) LayoutInflater.from(mContext).inflate( 320 R.layout.notification_center_activity, container, 321 /* attachToRoot= */ false); 322 mNotificationView.setKeyEventHandler( 323 event -> { 324 if (event.getKeyCode() != KeyEvent.KEYCODE_BACK) { 325 return false; 326 } 327 328 if (event.getAction() == KeyEvent.ACTION_UP && isPanelExpanded()) { 329 toggle(); 330 } 331 return true; 332 }); 333 334 container.addView(mNotificationView); 335 onNotificationViewInflated(); 336 } 337 onNotificationViewInflated()338 private void onNotificationViewInflated() { 339 // Find views. 340 mNotificationView = getLayout().findViewById(R.id.notification_view); 341 setUpHandleBar(); 342 setupNotificationPanel(); 343 344 mNotificationClickHandlerFactory.registerClickListener(mOnNotificationClickListener); 345 346 mNotificationDataManager.setOnUnseenCountUpdateListener(() -> { 347 if (mUnseenCountUpdateListener != null) { 348 // Don't show unseen markers for <= LOW importance notifications to be consistent 349 // with how these notifications are handled on phones 350 int unseenCount = 351 mNotificationDataManager.getNonLowImportanceUnseenNotificationCount( 352 mCarNotificationListener.getCurrentRanking()); 353 mUnseenCountUpdateListener.onUnseenCountUpdate(unseenCount); 354 } 355 if (isPanelExpanded()) { 356 // only report the seen notifications when the panel is expanded 357 mCarNotificationListener.setNotificationsShown( 358 mNotificationDataManager.getSeenNotifications()); 359 } 360 // This logs both when the notification panel is expanded and when the notification 361 // panel is scrolled. 362 mNotificationVisibilityLogger.log(isPanelExpanded()); 363 }); 364 365 mNotificationView.setClickHandlerFactory(mNotificationClickHandlerFactory); 366 mNotificationViewController = new NotificationViewController( 367 mNotificationView, 368 PreprocessingManager.getInstance(mContext), 369 mCarNotificationListener, 370 mCarUxRestrictionManagerWrapper); 371 372 if (!mOnConnectListenerAdded) { 373 mCarServiceProvider.addListener(mCarConnectedListener); 374 mOnConnectListenerAdded = true; 375 } 376 } 377 setupNotificationPanel()378 private void setupNotificationPanel() { 379 View glassPane = mNotificationView.findViewById(R.id.glass_pane); 380 mNotificationList = mNotificationView.findViewById(R.id.notifications); 381 GestureDetector closeGestureDetector = new GestureDetector(mContext, 382 new CloseGestureListener() { 383 @Override 384 protected void close() { 385 if (isPanelExpanded()) { 386 animateCollapsePanel(); 387 } 388 } 389 }); 390 391 // The glass pane is used to view touch events before passed to the notification list. 392 // This allows us to initialize gesture listeners and detect when to close the notifications 393 glassPane.setOnTouchListener((v, event) -> { 394 if (isClosingAction(event)) { 395 mNotificationListAtEndAtTimeOfTouch = false; 396 } 397 if (isOpeningAction(event)) { 398 mFirstTouchDownOnGlassPane = event.getRawX(); 399 mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd; 400 // Reset the tracker when there is a touch down on the glass pane. 401 setIsTracking(false); 402 // Pass the down event to gesture detector so that it knows where the touch event 403 // started. 404 closeGestureDetector.onTouchEvent(event); 405 } 406 return false; 407 }); 408 409 mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() { 410 @Override 411 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 412 super.onScrolled(recyclerView, dx, dy); 413 // Check if we can scroll vertically in the animation direction. 414 if (!mNotificationList.canScrollVertically(mAnimateDirection)) { 415 mNotificationListAtEnd = true; 416 return; 417 } 418 mNotificationListAtEnd = false; 419 mIsSwipingVerticallyToClose = false; 420 mNotificationListAtEndAtTimeOfTouch = false; 421 } 422 }); 423 424 mNotificationList.setOnTouchListener((v, event) -> { 425 mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX()) 426 > SWIPE_MAX_OFF_PATH; 427 if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) { 428 // We need to save the state here as if notification card is swiping we will 429 // change the mNotificationListAtEndAtTimeOfTouch. This is to protect 430 // closing the notification shade while the notification card is being swiped. 431 mIsSwipingVerticallyToClose = true; 432 } 433 434 // If the card is swiping we should not allow the notification shade to close. 435 // Hence setting mNotificationListAtEndAtTimeOfTouch to false will stop that 436 // for us. We are also checking for isTracking() because while swiping the 437 // notification shade to close if the user goes a bit horizontal while swiping 438 // upwards then also this should close. 439 if (mIsNotificationCardSwiping && !isTracking()) { 440 mNotificationListAtEndAtTimeOfTouch = false; 441 } 442 443 boolean handled = closeGestureDetector.onTouchEvent(event); 444 boolean isTracking = isTracking(); 445 Rect rect = getLayout().getClipBounds(); 446 float clippedHeight = 0; 447 if (rect != null) { 448 clippedHeight = rect.bottom; 449 } 450 if (!handled && isClosingAction(event) && mIsSwipingVerticallyToClose) { 451 if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) { 452 animatePanel(DEFAULT_FLING_VELOCITY, false); 453 } else if (clippedHeight != getLayout().getHeight() && isTracking) { 454 // this can be caused when user is at the end of the list and trying to 455 // fling to top of the list by scrolling down. 456 animatePanel(DEFAULT_FLING_VELOCITY, true); 457 } 458 } 459 460 // Updating the mNotificationListAtEndAtTimeOfTouch state has to be done after 461 // the event has been passed to the closeGestureDetector above, such that the 462 // closeGestureDetector sees the up event before the state has changed. 463 if (isClosingAction(event)) { 464 mNotificationListAtEndAtTimeOfTouch = false; 465 } 466 return handled || isTracking; 467 }); 468 } 469 470 /** Called when the car power state is changed to ON. */ onCarPowerStateOn()471 public void onCarPowerStateOn() { 472 if (mNotificationClickHandlerFactory != null) { 473 mNotificationClickHandlerFactory.clearAllNotifications(mContext); 474 } 475 mNotificationDataManager.clearAll(); 476 } 477 478 /** 479 * Forwards the call to clear all Notification cache. 480 * Note: This is a blocking call so should not execute any long-running or time-consuming tasks 481 * like storing cache. 482 */ clearCache()483 public void clearCache() { 484 mCarNotificationListener.clearCache(); 485 } 486 487 // OverlayPanelViewController 488 489 @Override shouldAnimateCollapsePanel()490 protected boolean shouldAnimateCollapsePanel() { 491 return true; 492 } 493 494 @Override onAnimateCollapsePanel()495 protected void onAnimateCollapsePanel() { 496 // no-op 497 } 498 499 @Override shouldAnimateExpandPanel()500 protected boolean shouldAnimateExpandPanel() { 501 return mCommandQueue.panelsEnabled(); 502 } 503 504 @Override onAnimateExpandPanel()505 protected void onAnimateExpandPanel() { 506 mNotificationList.scrollToPosition(0); 507 } 508 509 @Override getSettleClosePercentage()510 protected int getSettleClosePercentage() { 511 return mResources.getInteger(R.integer.notification_settle_close_percentage); 512 } 513 514 @Override onCollapseAnimationEnd()515 protected void onCollapseAnimationEnd() { 516 mNotificationViewController.onVisibilityChanged(false); 517 } 518 519 @Override onExpandAnimationEnd()520 protected void onExpandAnimationEnd() { 521 mNotificationView.setVisibleNotificationsAsSeen(); 522 mNotificationViewController.onVisibilityChanged(true); 523 } 524 525 @Override onPanelVisible(boolean visible)526 protected void onPanelVisible(boolean visible) { 527 super.onPanelVisible(visible); 528 mUiBgExecutor.execute(() -> { 529 try { 530 if (visible) { 531 // When notification panel is open even just a bit, we want to clear 532 // notification effects. 533 boolean clearNotificationEffects = 534 mStatusBarStateController.getState() != StatusBarState.KEYGUARD; 535 mBarService.onPanelRevealed(clearNotificationEffects, 536 mNotificationDataManager.getVisibleNotifications().size()); 537 } else { 538 mBarService.onPanelHidden(); 539 } 540 } catch (RemoteException ex) { 541 // Won't fail unless the world has ended. 542 Log.e(TAG, String.format( 543 "Unable to notify StatusBarService of panel visibility: %s", visible)); 544 } 545 }); 546 547 } 548 549 @Override onPanelExpanded(boolean expand)550 protected void onPanelExpanded(boolean expand) { 551 super.onPanelExpanded(expand); 552 553 if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) { 554 if (DEBUG) { 555 Log.v(TAG, "clearing notification effects from setExpandedHeight"); 556 } 557 clearNotificationEffects(); 558 } 559 if (!expand) { 560 mNotificationVisibilityLogger.log(isPanelExpanded()); 561 } 562 } 563 564 /** 565 * Clear Buzz/Beep/Blink. 566 */ clearNotificationEffects()567 private void clearNotificationEffects() { 568 try { 569 mBarService.clearNotificationEffects(); 570 } catch (RemoteException e) { 571 // Won't fail unless the world has ended. 572 } 573 } 574 575 @Override onOpenScrollStart()576 protected void onOpenScrollStart() { 577 mNotificationList.scrollToPosition(0); 578 } 579 580 @Override onScroll(int y)581 protected void onScroll(int y) { 582 super.onScroll(y); 583 584 if (mNotificationView.getHeight() > 0) { 585 Drawable background = mNotificationView.getBackground().mutate(); 586 background.setAlpha((int) (getBackgroundAlpha(y) * 255)); 587 mNotificationView.setBackground(background); 588 } 589 } 590 591 @Override shouldAllowClosingScroll()592 protected boolean shouldAllowClosingScroll() { 593 // Unless the notification list is at the end, the panel shouldn't be allowed to 594 // collapse on scroll. 595 return mNotificationListAtEndAtTimeOfTouch; 596 } 597 598 @Override getHandleBarViewId()599 protected Integer getHandleBarViewId() { 600 return R.id.handle_bar; 601 } 602 603 /** 604 * Calculates the alpha value for the background based on how much of the notification 605 * shade is visible to the user. When the notification shade is completely open then 606 * alpha value will be 1. 607 */ getBackgroundAlpha(int y)608 private float getBackgroundAlpha(int y) { 609 float fractionCovered = 610 ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y)) 611 / mNotificationView.getHeight(); 612 return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff; 613 } 614 615 /** Sets the unseen count listener. */ setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener)616 public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) { 617 mUnseenCountUpdateListener = listener; 618 } 619 620 /** Listener that is updated when the number of unseen notifications changes. */ 621 public interface OnUnseenCountUpdateListener { 622 /** 623 * This method is automatically called whenever there is an update to the number of unseen 624 * notifications. This method can be extended by OEMs to customize the desired logic. 625 */ onUnseenCountUpdate(int unseenNotificationCount)626 void onUnseenCountUpdate(int unseenNotificationCount); 627 } 628 } 629