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 mNotificationViewController = new NotificationViewController( 332 mNotificationView, 333 PreprocessingManager.getInstance(mContext), 334 mCarNotificationListener, 335 mCarUxRestrictionManagerWrapper); 336 337 mCarServiceProvider.addListener(car -> { 338 CarUxRestrictionsManager carUxRestrictionsManager = 339 (CarUxRestrictionsManager) 340 car.getCarManager(Car.CAR_UX_RESTRICTION_SERVICE); 341 mCarUxRestrictionManagerWrapper.setCarUxRestrictionsManager( 342 carUxRestrictionsManager); 343 344 PreprocessingManager preprocessingManager = PreprocessingManager.getInstance(mContext); 345 preprocessingManager.setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper); 346 347 mNotificationViewController.enable(); 348 }); 349 } 350 setupNotificationPanel()351 private void setupNotificationPanel() { 352 View glassPane = mNotificationView.findViewById(R.id.glass_pane); 353 mNotificationList = mNotificationView.findViewById(R.id.notifications); 354 GestureDetector closeGestureDetector = new GestureDetector(mContext, 355 new CloseGestureListener() { 356 @Override 357 protected void close() { 358 if (isPanelExpanded()) { 359 animateCollapsePanel(); 360 } 361 } 362 }); 363 364 // The glass pane is used to view touch events before passed to the notification list. 365 // This allows us to initialize gesture listeners and detect when to close the notifications 366 glassPane.setOnTouchListener((v, event) -> { 367 if (isClosingAction(event)) { 368 mNotificationListAtEndAtTimeOfTouch = false; 369 } 370 if (isOpeningAction(event)) { 371 mFirstTouchDownOnGlassPane = event.getRawX(); 372 mNotificationListAtEndAtTimeOfTouch = mNotificationListAtEnd; 373 // Reset the tracker when there is a touch down on the glass pane. 374 setIsTracking(false); 375 // Pass the down event to gesture detector so that it knows where the touch event 376 // started. 377 closeGestureDetector.onTouchEvent(event); 378 } 379 return false; 380 }); 381 382 mNotificationList.addOnScrollListener(new RecyclerView.OnScrollListener() { 383 @Override 384 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 385 super.onScrolled(recyclerView, dx, dy); 386 // Check if we can scroll vertically in the animation direction. 387 if (!mNotificationList.canScrollVertically(mAnimateDirection)) { 388 mNotificationListAtEnd = true; 389 return; 390 } 391 mNotificationListAtEnd = false; 392 mIsSwipingVerticallyToClose = false; 393 mNotificationListAtEndAtTimeOfTouch = false; 394 } 395 }); 396 397 mNotificationList.setOnTouchListener((v, event) -> { 398 mIsNotificationCardSwiping = Math.abs(mFirstTouchDownOnGlassPane - event.getRawX()) 399 > SWIPE_MAX_OFF_PATH; 400 if (mNotificationListAtEndAtTimeOfTouch && mNotificationListAtEnd) { 401 // We need to save the state here as if notification card is swiping we will 402 // change the mNotificationListAtEndAtTimeOfTouch. This is to protect 403 // closing the notification shade while the notification card is being swiped. 404 mIsSwipingVerticallyToClose = true; 405 } 406 407 // If the card is swiping we should not allow the notification shade to close. 408 // Hence setting mNotificationListAtEndAtTimeOfTouch to false will stop that 409 // for us. We are also checking for isTracking() because while swiping the 410 // notification shade to close if the user goes a bit horizontal while swiping 411 // upwards then also this should close. 412 if (mIsNotificationCardSwiping && !isTracking()) { 413 mNotificationListAtEndAtTimeOfTouch = false; 414 } 415 416 boolean handled = closeGestureDetector.onTouchEvent(event); 417 boolean isTracking = isTracking(); 418 Rect rect = getLayout().getClipBounds(); 419 float clippedHeight = 0; 420 if (rect != null) { 421 clippedHeight = rect.bottom; 422 } 423 if (!handled && isClosingAction(event) && mIsSwipingVerticallyToClose) { 424 if (getSettleClosePercentage() < getPercentageFromEndingEdge() && isTracking) { 425 animatePanel(DEFAULT_FLING_VELOCITY, false); 426 } else if (clippedHeight != getLayout().getHeight() && isTracking) { 427 // this can be caused when user is at the end of the list and trying to 428 // fling to top of the list by scrolling down. 429 animatePanel(DEFAULT_FLING_VELOCITY, true); 430 } 431 } 432 433 // Updating the mNotificationListAtEndAtTimeOfTouch state has to be done after 434 // the event has been passed to the closeGestureDetector above, such that the 435 // closeGestureDetector sees the up event before the state has changed. 436 if (isClosingAction(event)) { 437 mNotificationListAtEndAtTimeOfTouch = false; 438 } 439 return handled || isTracking; 440 }); 441 } 442 443 /** Called when the car power state is changed to ON. */ onCarPowerStateOn()444 public void onCarPowerStateOn() { 445 if (mNotificationClickHandlerFactory != null) { 446 mNotificationClickHandlerFactory.clearAllNotifications(); 447 } 448 mNotificationDataManager.clearAll(); 449 } 450 451 // OverlayPanelViewController 452 453 @Override shouldAnimateCollapsePanel()454 protected boolean shouldAnimateCollapsePanel() { 455 return true; 456 } 457 458 @Override onAnimateCollapsePanel()459 protected void onAnimateCollapsePanel() { 460 // no-op 461 } 462 463 @Override shouldAnimateExpandPanel()464 protected boolean shouldAnimateExpandPanel() { 465 return mCommandQueue.panelsEnabled(); 466 } 467 468 @Override onAnimateExpandPanel()469 protected void onAnimateExpandPanel() { 470 mNotificationList.scrollToPosition(0); 471 } 472 473 @Override getSettleClosePercentage()474 protected int getSettleClosePercentage() { 475 return mResources.getInteger(R.integer.notification_settle_close_percentage); 476 } 477 478 @Override onCollapseAnimationEnd()479 protected void onCollapseAnimationEnd() { 480 mNotificationViewController.onVisibilityChanged(false); 481 } 482 483 @Override onExpandAnimationEnd()484 protected void onExpandAnimationEnd() { 485 mNotificationView.setVisibleNotificationsAsSeen(); 486 mNotificationViewController.onVisibilityChanged(true); 487 } 488 489 @Override onPanelVisible(boolean visible)490 protected void onPanelVisible(boolean visible) { 491 super.onPanelVisible(visible); 492 mUiBgExecutor.execute(() -> { 493 try { 494 if (visible) { 495 // When notification panel is open even just a bit, we want to clear 496 // notification effects. 497 boolean clearNotificationEffects = 498 mStatusBarStateController.getState() != StatusBarState.KEYGUARD; 499 mBarService.onPanelRevealed(clearNotificationEffects, 500 mNotificationDataManager.getVisibleNotifications().size()); 501 } else { 502 mBarService.onPanelHidden(); 503 } 504 } catch (RemoteException ex) { 505 // Won't fail unless the world has ended. 506 Log.e(TAG, String.format( 507 "Unable to notify StatusBarService of panel visibility: %s", visible)); 508 } 509 }); 510 511 } 512 513 @Override onPanelExpanded(boolean expand)514 protected void onPanelExpanded(boolean expand) { 515 super.onPanelExpanded(expand); 516 517 if (expand && mStatusBarStateController.getState() != StatusBarState.KEYGUARD) { 518 if (DEBUG) { 519 Log.v(TAG, "clearing notification effects from setExpandedHeight"); 520 } 521 clearNotificationEffects(); 522 } 523 if (!expand) { 524 mNotificationVisibilityLogger.log(isPanelExpanded()); 525 } 526 } 527 528 /** 529 * Clear Buzz/Beep/Blink. 530 */ clearNotificationEffects()531 private void clearNotificationEffects() { 532 try { 533 mBarService.clearNotificationEffects(); 534 } catch (RemoteException e) { 535 // Won't fail unless the world has ended. 536 } 537 } 538 539 @Override onOpenScrollStart()540 protected void onOpenScrollStart() { 541 mNotificationList.scrollToPosition(0); 542 } 543 544 @Override onScroll(int y)545 protected void onScroll(int y) { 546 super.onScroll(y); 547 548 if (mNotificationView.getHeight() > 0) { 549 Drawable background = mNotificationView.getBackground().mutate(); 550 background.setAlpha((int) (getBackgroundAlpha(y) * 255)); 551 mNotificationView.setBackground(background); 552 } 553 } 554 555 @Override shouldAllowClosingScroll()556 protected boolean shouldAllowClosingScroll() { 557 // Unless the notification list is at the end, the panel shouldn't be allowed to 558 // collapse on scroll. 559 return mNotificationListAtEndAtTimeOfTouch; 560 } 561 562 @Override getHandleBarViewId()563 protected Integer getHandleBarViewId() { 564 return R.id.handle_bar; 565 } 566 567 /** 568 * Calculates the alpha value for the background based on how much of the notification 569 * shade is visible to the user. When the notification shade is completely open then 570 * alpha value will be 1. 571 */ getBackgroundAlpha(int y)572 private float getBackgroundAlpha(int y) { 573 float fractionCovered = 574 ((float) (mAnimateDirection > 0 ? y : mNotificationView.getHeight() - y)) 575 / mNotificationView.getHeight(); 576 return mInitialBackgroundAlpha + fractionCovered * mBackgroundAlphaDiff; 577 } 578 579 /** Sets the unseen count listener. */ setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener)580 public void setOnUnseenCountUpdateListener(OnUnseenCountUpdateListener listener) { 581 mUnseenCountUpdateListener = listener; 582 } 583 584 /** Listener that is updated when the number of unseen notifications changes. */ 585 public interface OnUnseenCountUpdateListener { 586 /** 587 * This method is automatically called whenever there is an update to the number of unseen 588 * notifications. This method can be extended by OEMs to customize the desired logic. 589 */ onUnseenCountUpdate(int unseenNotificationCount)590 void onUnseenCountUpdate(int unseenNotificationCount); 591 } 592 } 593