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