1 /* 2 * Copyright (C) 2012 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.bubbles; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.ValueAnimator; 25 import android.annotation.NonNull; 26 import android.app.Notification; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.ColorMatrix; 30 import android.graphics.ColorMatrixColorFilter; 31 import android.graphics.Outline; 32 import android.graphics.Paint; 33 import android.graphics.Point; 34 import android.graphics.PointF; 35 import android.graphics.Rect; 36 import android.graphics.RectF; 37 import android.os.Bundle; 38 import android.os.VibrationEffect; 39 import android.os.Vibrator; 40 import android.service.notification.StatusBarNotification; 41 import android.util.Log; 42 import android.util.StatsLog; 43 import android.view.Choreographer; 44 import android.view.Gravity; 45 import android.view.LayoutInflater; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.ViewOutlineProvider; 49 import android.view.ViewTreeObserver; 50 import android.view.WindowInsets; 51 import android.view.WindowManager; 52 import android.view.accessibility.AccessibilityNodeInfo; 53 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 54 import android.view.animation.AccelerateDecelerateInterpolator; 55 import android.widget.FrameLayout; 56 57 import androidx.annotation.MainThread; 58 import androidx.annotation.Nullable; 59 import androidx.dynamicanimation.animation.DynamicAnimation; 60 import androidx.dynamicanimation.animation.FloatPropertyCompat; 61 import androidx.dynamicanimation.animation.SpringAnimation; 62 import androidx.dynamicanimation.animation.SpringForce; 63 64 import com.android.internal.annotations.VisibleForTesting; 65 import com.android.internal.widget.ViewClippingUtil; 66 import com.android.systemui.R; 67 import com.android.systemui.bubbles.animation.ExpandedAnimationController; 68 import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; 69 import com.android.systemui.bubbles.animation.StackAnimationController; 70 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 71 72 import java.math.BigDecimal; 73 import java.math.RoundingMode; 74 import java.util.ArrayList; 75 import java.util.Collections; 76 import java.util.List; 77 78 /** 79 * Renders bubbles in a stack and handles animating expanded and collapsed states. 80 */ 81 public class BubbleStackView extends FrameLayout { 82 private static final String TAG = "BubbleStackView"; 83 private static final boolean DEBUG = false; 84 85 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ 86 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; 87 88 /** Velocity required to dismiss the flyout via drag. */ 89 private static final float FLYOUT_DISMISS_VELOCITY = 2000f; 90 91 /** 92 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel 93 * for every 8 pixels overscrolled). 94 */ 95 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; 96 97 /** Duration of the flyout alpha animations. */ 98 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; 99 100 /** Percent to darken the bubbles when they're in the dismiss target. */ 101 private static final float DARKEN_PERCENT = 0.3f; 102 103 /** How long to wait, in milliseconds, before hiding the flyout. */ 104 @VisibleForTesting 105 static final int FLYOUT_HIDE_AFTER = 5000; 106 107 /** 108 * Interface to synchronize {@link View} state and the screen. 109 * 110 * {@hide} 111 */ 112 interface SurfaceSynchronizer { 113 /** 114 * Wait until requested change on a {@link View} is reflected on the screen. 115 * 116 * @param callback callback to run after the change is reflected on the screen. 117 */ syncSurfaceAndRun(Runnable callback)118 void syncSurfaceAndRun(Runnable callback); 119 } 120 121 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = 122 new SurfaceSynchronizer() { 123 @Override 124 public void syncSurfaceAndRun(Runnable callback) { 125 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { 126 // Just wait 2 frames. There is no guarantee, but this is usually enough time that 127 // the requested change is reflected on the screen. 128 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and 129 // surfaces, rewrite this logic with them. 130 private int mFrameWait = 2; 131 132 @Override 133 public void doFrame(long frameTimeNanos) { 134 if (--mFrameWait > 0) { 135 Choreographer.getInstance().postFrameCallback(this); 136 } else { 137 callback.run(); 138 } 139 } 140 }); 141 } 142 }; 143 144 private Point mDisplaySize; 145 146 private final SpringAnimation mExpandedViewXAnim; 147 private final SpringAnimation mExpandedViewYAnim; 148 private final BubbleData mBubbleData; 149 150 private final Vibrator mVibrator; 151 private final ValueAnimator mDesaturateAndDarkenAnimator; 152 private final Paint mDesaturateAndDarkenPaint = new Paint(); 153 154 private PhysicsAnimationLayout mBubbleContainer; 155 private StackAnimationController mStackAnimationController; 156 private ExpandedAnimationController mExpandedAnimationController; 157 158 private FrameLayout mExpandedViewContainer; 159 160 private BubbleFlyoutView mFlyout; 161 /** Runnable that fades out the flyout and then sets it to GONE. */ 162 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); 163 164 /** Layout change listener that moves the stack to the nearest valid position on rotation. */ 165 private OnLayoutChangeListener mMoveStackToValidPositionOnLayoutListener; 166 /** Whether the stack was on the left side of the screen prior to rotation. */ 167 private boolean mWasOnLeftBeforeRotation = false; 168 /** 169 * How far down the screen the stack was before rotation, in terms of percentage of the way down 170 * the allowable region. Defaults to -1 if not set. 171 */ 172 private float mVerticalPosPercentBeforeRotation = -1; 173 174 private int mBubbleSize; 175 private int mBubblePadding; 176 private int mExpandedViewPadding; 177 private int mExpandedAnimateXDistance; 178 private int mExpandedAnimateYDistance; 179 private int mPointerHeight; 180 private int mStatusBarHeight; 181 private int mPipDismissHeight; 182 private int mImeOffset; 183 184 private Bubble mExpandedBubble; 185 private boolean mIsExpanded; 186 private boolean mImeVisible; 187 188 /** Whether the stack is currently on the left side of the screen, or animating there. */ 189 private boolean mStackOnLeftOrWillBe = false; 190 191 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ 192 private boolean mIsGestureInProgress = false; 193 194 private BubbleTouchHandler mTouchHandler; 195 private BubbleController.BubbleExpandListener mExpandListener; 196 private BubbleExpandedView.OnBubbleBlockedListener mBlockedListener; 197 198 private boolean mViewUpdatedRequested = false; 199 private boolean mIsExpansionAnimating = false; 200 private boolean mShowingDismiss = false; 201 202 /** 203 * Whether the user is currently dragging their finger within the dismiss target. In this state 204 * the stack will be magnetized to the center of the target, so we shouldn't move it until the 205 * touch exits the dismiss target area. 206 */ 207 private boolean mDraggingInDismissTarget = false; 208 209 /** Whether the stack is magneting towards the dismiss target. */ 210 private boolean mAnimatingMagnet = false; 211 212 /** The view to desaturate/darken when magneted to the dismiss target. */ 213 private View mDesaturateAndDarkenTargetView; 214 215 private LayoutInflater mInflater; 216 217 // Used for determining view / touch intersection 218 int[] mTempLoc = new int[2]; 219 RectF mTempRect = new RectF(); 220 221 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); 222 223 private ViewTreeObserver.OnPreDrawListener mViewUpdater = 224 new ViewTreeObserver.OnPreDrawListener() { 225 @Override 226 public boolean onPreDraw() { 227 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 228 applyCurrentState(); 229 mViewUpdatedRequested = false; 230 return true; 231 } 232 }; 233 234 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = 235 this::updateSystemGestureExcludeRects; 236 237 private ViewClippingUtil.ClippingParameters mClippingParameters = 238 new ViewClippingUtil.ClippingParameters() { 239 240 @Override 241 public boolean shouldFinish(View view) { 242 return false; 243 } 244 245 @Override 246 public boolean isClippingEnablingAllowed(View view) { 247 return !mIsExpanded; 248 } 249 }; 250 251 /** Float property that 'drags' the flyout. */ 252 private final FloatPropertyCompat mFlyoutCollapseProperty = 253 new FloatPropertyCompat("FlyoutCollapseSpring") { 254 @Override 255 public float getValue(Object o) { 256 return mFlyoutDragDeltaX; 257 } 258 259 @Override 260 public void setValue(Object o, float v) { 261 onFlyoutDragged(v); 262 } 263 }; 264 265 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ 266 private final SpringAnimation mFlyoutTransitionSpring = 267 new SpringAnimation(this, mFlyoutCollapseProperty); 268 269 /** Distance the flyout has been dragged in the X axis. */ 270 private float mFlyoutDragDeltaX = 0f; 271 272 /** 273 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides 274 * it immediately. 275 */ 276 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = 277 (dynamicAnimation, b, v, v1) -> { 278 if (mFlyoutDragDeltaX == 0) { 279 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 280 } else { 281 mFlyout.hideFlyout(); 282 } 283 }; 284 285 @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer; 286 287 private BubbleDismissView mDismissContainer; 288 private Runnable mAfterMagnet; 289 290 private boolean mSuppressNewDot = false; 291 private boolean mSuppressFlyout = false; 292 BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer)293 public BubbleStackView(Context context, BubbleData data, 294 @Nullable SurfaceSynchronizer synchronizer) { 295 super(context); 296 297 mBubbleData = data; 298 mInflater = LayoutInflater.from(context); 299 mTouchHandler = new BubbleTouchHandler(this, data, context); 300 setOnTouchListener(mTouchHandler); 301 mInflater = LayoutInflater.from(context); 302 303 Resources res = getResources(); 304 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); 305 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding); 306 mExpandedAnimateXDistance = 307 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance); 308 mExpandedAnimateYDistance = 309 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance); 310 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); 311 312 mStatusBarHeight = 313 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 314 mPipDismissHeight = mContext.getResources().getDimensionPixelSize( 315 R.dimen.pip_dismiss_gradient_height); 316 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); 317 318 mDisplaySize = new Point(); 319 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 320 wm.getDefaultDisplay().getSize(mDisplaySize); 321 322 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 323 324 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 325 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 326 327 mStackAnimationController = new StackAnimationController(); 328 mExpandedAnimationController = new ExpandedAnimationController( 329 mDisplaySize, mExpandedViewPadding); 330 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; 331 332 mBubbleContainer = new PhysicsAnimationLayout(context); 333 mBubbleContainer.setActiveController(mStackAnimationController); 334 mBubbleContainer.setElevation(elevation); 335 mBubbleContainer.setClipChildren(false); 336 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 337 338 mExpandedViewContainer = new FrameLayout(context); 339 mExpandedViewContainer.setElevation(elevation); 340 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding, 341 mExpandedViewPadding, mExpandedViewPadding); 342 mExpandedViewContainer.setClipChildren(false); 343 addView(mExpandedViewContainer); 344 345 mFlyout = new BubbleFlyoutView(context); 346 mFlyout.setVisibility(GONE); 347 mFlyout.animate() 348 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION) 349 .setInterpolator(new AccelerateDecelerateInterpolator()); 350 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 351 352 mFlyoutTransitionSpring.setSpring(new SpringForce() 353 .setStiffness(SpringForce.STIFFNESS_MEDIUM) 354 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 355 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); 356 357 mDismissContainer = new BubbleDismissView(mContext); 358 mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams( 359 MATCH_PARENT, 360 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height), 361 Gravity.BOTTOM)); 362 addView(mDismissContainer); 363 364 mDismissContainer = new BubbleDismissView(mContext); 365 mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams( 366 MATCH_PARENT, 367 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height), 368 Gravity.BOTTOM)); 369 addView(mDismissContainer); 370 371 mExpandedViewXAnim = 372 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X); 373 mExpandedViewXAnim.setSpring( 374 new SpringForce() 375 .setStiffness(SpringForce.STIFFNESS_LOW) 376 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 377 378 mExpandedViewYAnim = 379 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y); 380 mExpandedViewYAnim.setSpring( 381 new SpringForce() 382 .setStiffness(SpringForce.STIFFNESS_LOW) 383 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 384 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> { 385 if (mIsExpanded && mExpandedBubble != null) { 386 mExpandedBubble.expandedView.updateView(); 387 } 388 }); 389 390 setClipChildren(false); 391 setFocusable(true); 392 mBubbleContainer.bringToFront(); 393 394 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { 395 final int keyboardHeight = insets.getSystemWindowInsetBottom() 396 - insets.getStableInsetBottom(); 397 if (!mIsExpanded || mIsExpansionAnimating) { 398 return view.onApplyWindowInsets(insets); 399 } 400 mImeVisible = keyboardHeight != 0; 401 402 float newY = getYPositionForExpandedView(); 403 if (newY < 0) { 404 // TODO: This means our expanded content is too big to fit on screen. Right now 405 // we'll let it translate off but we should be clipping it & pushing the header 406 // down so that it always remains visible. 407 } 408 mExpandedViewYAnim.animateToFinalPosition(newY); 409 mExpandedAnimationController.updateYPosition( 410 // Update the insets after we're done translating otherwise position 411 // calculation for them won't be correct. 412 () -> mExpandedBubble.expandedView.updateInsets(insets)); 413 return view.onApplyWindowInsets(insets); 414 }); 415 416 mMoveStackToValidPositionOnLayoutListener = 417 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 418 if (mVerticalPosPercentBeforeRotation >= 0) { 419 mStackAnimationController.moveStackToSimilarPositionAfterRotation( 420 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation); 421 } 422 removeOnLayoutChangeListener(mMoveStackToValidPositionOnLayoutListener); 423 }; 424 425 // This must be a separate OnDrawListener since it should be called for every draw. 426 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); 427 428 final ColorMatrix animatedMatrix = new ColorMatrix(); 429 final ColorMatrix darkenMatrix = new ColorMatrix(); 430 431 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f); 432 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> { 433 final float animatedValue = (float) animation.getAnimatedValue(); 434 animatedMatrix.setSaturation(animatedValue); 435 436 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT; 437 darkenMatrix.setScale( 438 1f - animatedDarkenValue /* red */, 439 1f - animatedDarkenValue /* green */, 440 1f - animatedDarkenValue /* blue */, 441 1f /* alpha */); 442 443 // Concat the matrices so that the animatedMatrix both desaturates and darkens. 444 animatedMatrix.postConcat(darkenMatrix); 445 446 // Update the paint and apply it to the bubble container. 447 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix)); 448 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint); 449 }); 450 } 451 452 /** 453 * Handle theme changes. 454 */ onThemeChanged()455 public void onThemeChanged() { 456 for (Bubble b: mBubbleData.getBubbles()) { 457 b.iconView.updateViews(); 458 b.expandedView.applyThemeAttrs(); 459 } 460 } 461 462 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ onOrientationChanged()463 public void onOrientationChanged() { 464 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion(); 465 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide(); 466 mVerticalPosPercentBeforeRotation = 467 (mStackAnimationController.getStackPosition().y - allowablePos.top) 468 / (allowablePos.bottom - allowablePos.top); 469 addOnLayoutChangeListener(mMoveStackToValidPositionOnLayoutListener); 470 471 hideFlyoutImmediate(); 472 } 473 474 @Override getBoundsOnScreen(Rect outRect, boolean clipToParent)475 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) { 476 getBoundsOnScreen(outRect); 477 } 478 479 @Override onDetachedFromWindow()480 protected void onDetachedFromWindow() { 481 super.onDetachedFromWindow(); 482 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 483 } 484 485 @Override onInterceptTouchEvent(MotionEvent ev)486 public boolean onInterceptTouchEvent(MotionEvent ev) { 487 float x = ev.getRawX(); 488 float y = ev.getRawY(); 489 // If we're expanded only intercept if the tap is outside of the widget container 490 if (mIsExpanded && isIntersecting(mExpandedViewContainer, x, y)) { 491 return false; 492 } else { 493 return isIntersecting(mBubbleContainer, x, y); 494 } 495 } 496 497 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)498 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 499 super.onInitializeAccessibilityNodeInfoInternal(info); 500 501 // Custom actions. 502 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left, 503 getContext().getResources() 504 .getString(R.string.bubble_accessibility_action_move_top_left)); 505 info.addAction(moveTopLeft); 506 507 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right, 508 getContext().getResources() 509 .getString(R.string.bubble_accessibility_action_move_top_right)); 510 info.addAction(moveTopRight); 511 512 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left, 513 getContext().getResources() 514 .getString(R.string.bubble_accessibility_action_move_bottom_left)); 515 info.addAction(moveBottomLeft); 516 517 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right, 518 getContext().getResources() 519 .getString(R.string.bubble_accessibility_action_move_bottom_right)); 520 info.addAction(moveBottomRight); 521 522 // Default actions. 523 info.addAction(AccessibilityAction.ACTION_DISMISS); 524 if (mIsExpanded) { 525 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 526 } else { 527 info.addAction(AccessibilityAction.ACTION_EXPAND); 528 } 529 } 530 531 @Override performAccessibilityActionInternal(int action, Bundle arguments)532 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 533 if (super.performAccessibilityActionInternal(action, arguments)) { 534 return true; 535 } 536 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); 537 538 // R constants are not final so we cannot use switch-case here. 539 if (action == AccessibilityNodeInfo.ACTION_DISMISS) { 540 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION); 541 return true; 542 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { 543 mBubbleData.setExpanded(false); 544 return true; 545 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) { 546 mBubbleData.setExpanded(true); 547 return true; 548 } else if (action == R.id.action_move_top_left) { 549 mStackAnimationController.springStack(stackBounds.left, stackBounds.top); 550 return true; 551 } else if (action == R.id.action_move_top_right) { 552 mStackAnimationController.springStack(stackBounds.right, stackBounds.top); 553 return true; 554 } else if (action == R.id.action_move_bottom_left) { 555 mStackAnimationController.springStack(stackBounds.left, stackBounds.bottom); 556 return true; 557 } else if (action == R.id.action_move_bottom_right) { 558 mStackAnimationController.springStack(stackBounds.right, stackBounds.bottom); 559 return true; 560 } 561 return false; 562 } 563 564 /** 565 * Update content description for a11y TalkBack. 566 */ updateContentDescription()567 public void updateContentDescription() { 568 if (mBubbleData.getBubbles().isEmpty()) { 569 return; 570 } 571 Bubble topBubble = mBubbleData.getBubbles().get(0); 572 String appName = topBubble.getAppName(); 573 Notification notification = topBubble.entry.notification.getNotification(); 574 CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE); 575 String titleStr = getResources().getString(R.string.stream_notification); 576 if (titleCharSeq != null) { 577 titleStr = titleCharSeq.toString(); 578 } 579 int moreCount = mBubbleContainer.getChildCount() - 1; 580 581 // Example: Title from app name. 582 String singleDescription = getResources().getString( 583 R.string.bubble_content_description_single, titleStr, appName); 584 585 // Example: Title from app name and 4 more. 586 String stackDescription = getResources().getString( 587 R.string.bubble_content_description_stack, titleStr, appName, moreCount); 588 589 if (mIsExpanded) { 590 // TODO(b/129522932) - update content description for each bubble in expanded view. 591 } else { 592 // Collapsed stack. 593 if (moreCount > 0) { 594 mBubbleContainer.setContentDescription(stackDescription); 595 } else { 596 mBubbleContainer.setContentDescription(singleDescription); 597 } 598 } 599 } 600 updateSystemGestureExcludeRects()601 private void updateSystemGestureExcludeRects() { 602 // Exclude the region occupied by the first BubbleView in the stack 603 Rect excludeZone = mSystemGestureExclusionRects.get(0); 604 if (mBubbleContainer.getChildCount() > 0) { 605 View firstBubble = mBubbleContainer.getChildAt(0); 606 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(), 607 firstBubble.getBottom()); 608 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f), 609 (int) (firstBubble.getTranslationY() + 0.5f)); 610 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects); 611 } else { 612 excludeZone.setEmpty(); 613 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList()); 614 } 615 } 616 617 /** 618 * Updates the visibility of the 'dot' indicating an update on the bubble. 619 * @param key the {@link NotificationEntry#key} associated with the bubble. 620 */ updateDotVisibility(String key)621 public void updateDotVisibility(String key) { 622 Bubble b = mBubbleData.getBubbleWithKey(key); 623 if (b != null) { 624 b.updateDotVisibility(); 625 } 626 } 627 628 /** 629 * Sets the listener to notify when the bubble stack is expanded. 630 */ setExpandListener(BubbleController.BubbleExpandListener listener)631 public void setExpandListener(BubbleController.BubbleExpandListener listener) { 632 mExpandListener = listener; 633 } 634 635 /** 636 * Whether the stack of bubbles is expanded or not. 637 */ isExpanded()638 public boolean isExpanded() { 639 return mIsExpanded; 640 } 641 642 /** 643 * The {@link BubbleView} that is expanded, null if one does not exist. 644 */ getExpandedBubbleView()645 BubbleView getExpandedBubbleView() { 646 return mExpandedBubble != null ? mExpandedBubble.iconView : null; 647 } 648 649 /** 650 * The {@link Bubble} that is expanded, null if one does not exist. 651 */ getExpandedBubble()652 Bubble getExpandedBubble() { 653 return mExpandedBubble; 654 } 655 656 /** 657 * Sets the bubble that should be expanded and expands if needed. 658 * 659 * @param key the {@link NotificationEntry#key} associated with the bubble to expand. 660 * @deprecated replaced by setSelectedBubble(Bubble) + setExpanded(true) 661 */ 662 @Deprecated setExpandedBubble(String key)663 void setExpandedBubble(String key) { 664 Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key); 665 if (bubbleToExpand != null) { 666 setSelectedBubble(bubbleToExpand); 667 bubbleToExpand.entry.setShowInShadeWhenBubble(false); 668 setExpanded(true); 669 } 670 } 671 672 /** 673 * Sets the entry that should be expanded and expands if needed. 674 */ 675 @VisibleForTesting setExpandedBubble(NotificationEntry entry)676 void setExpandedBubble(NotificationEntry entry) { 677 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { 678 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); 679 if (entry.equals(bv.getEntry())) { 680 setExpandedBubble(entry.key); 681 } 682 } 683 } 684 685 // via BubbleData.Listener addBubble(Bubble bubble)686 void addBubble(Bubble bubble) { 687 if (DEBUG) { 688 Log.d(TAG, "addBubble: " + bubble); 689 } 690 bubble.inflate(mInflater, this); 691 mBubbleContainer.addView(bubble.iconView, 0, 692 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 693 ViewClippingUtil.setClippingDeactivated(bubble.iconView, true, mClippingParameters); 694 if (bubble.iconView != null) { 695 bubble.iconView.setSuppressDot(mSuppressNewDot, false /* animate */); 696 } 697 animateInFlyoutForBubble(bubble); 698 requestUpdate(); 699 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED); 700 updatePointerPosition(); 701 } 702 703 // via BubbleData.Listener removeBubble(Bubble bubble)704 void removeBubble(Bubble bubble) { 705 if (DEBUG) { 706 Log.d(TAG, "removeBubble: " + bubble); 707 } 708 // Remove it from the views 709 int removedIndex = mBubbleContainer.indexOfChild(bubble.iconView); 710 if (removedIndex >= 0) { 711 mBubbleContainer.removeViewAt(removedIndex); 712 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); 713 } else { 714 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); 715 } 716 updatePointerPosition(); 717 } 718 719 // via BubbleData.Listener updateBubble(Bubble bubble)720 void updateBubble(Bubble bubble) { 721 animateInFlyoutForBubble(bubble); 722 requestUpdate(); 723 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); 724 } 725 updateBubbleOrder(List<Bubble> bubbles)726 public void updateBubbleOrder(List<Bubble> bubbles) { 727 for (int i = 0; i < bubbles.size(); i++) { 728 Bubble bubble = bubbles.get(i); 729 mBubbleContainer.reorderView(bubble.iconView, i); 730 } 731 } 732 733 /** 734 * Changes the currently selected bubble. If the stack is already expanded, the newly selected 735 * bubble will be shown immediately. This does not change the expanded state or change the 736 * position of any bubble. 737 */ 738 // via BubbleData.Listener setSelectedBubble(@ullable Bubble bubbleToSelect)739 public void setSelectedBubble(@Nullable Bubble bubbleToSelect) { 740 if (DEBUG) { 741 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); 742 } 743 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) { 744 return; 745 } 746 final Bubble previouslySelected = mExpandedBubble; 747 mExpandedBubble = bubbleToSelect; 748 if (mIsExpanded) { 749 // Make the container of the expanded view transparent before removing the expanded view 750 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the 751 // expanded view becomes visible on the screen. See b/126856255 752 mExpandedViewContainer.setAlpha(0.0f); 753 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 754 updateExpandedBubble(); 755 updatePointerPosition(); 756 requestUpdate(); 757 logBubbleEvent(previouslySelected, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 758 logBubbleEvent(bubbleToSelect, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 759 notifyExpansionChanged(previouslySelected.entry, false /* expanded */); 760 notifyExpansionChanged(bubbleToSelect == null ? null : bubbleToSelect.entry, 761 true /* expanded */); 762 }); 763 } 764 } 765 766 /** 767 * Changes the expanded state of the stack. 768 * 769 * @param shouldExpand whether the bubble stack should appear expanded 770 */ 771 // via BubbleData.Listener setExpanded(boolean shouldExpand)772 public void setExpanded(boolean shouldExpand) { 773 if (DEBUG) { 774 Log.d(TAG, "setExpanded: " + shouldExpand); 775 } 776 boolean wasExpanded = mIsExpanded; 777 if (shouldExpand == wasExpanded) { 778 return; 779 } 780 if (wasExpanded) { 781 // Collapse the stack 782 animateExpansion(false /* expand */); 783 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 784 } else { 785 // Expand the stack 786 animateExpansion(true /* expand */); 787 // TODO: move next line to BubbleData 788 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 789 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); 790 } 791 notifyExpansionChanged(mExpandedBubble.entry, mIsExpanded); 792 } 793 794 /** 795 * Dismiss the stack of bubbles. 796 * @deprecated 797 */ 798 @Deprecated stackDismissed(int reason)799 void stackDismissed(int reason) { 800 if (DEBUG) { 801 Log.d(TAG, "stackDismissed: reason=" + reason); 802 } 803 mBubbleData.dismissAll(reason); 804 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */, 805 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED); 806 } 807 808 /** 809 * @return the view the touch event is on 810 */ 811 @Nullable getTargetView(MotionEvent event)812 public View getTargetView(MotionEvent event) { 813 float x = event.getRawX(); 814 float y = event.getRawY(); 815 if (mIsExpanded) { 816 if (isIntersecting(mBubbleContainer, x, y)) { 817 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { 818 BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i); 819 if (isIntersecting(view, x, y)) { 820 return view; 821 } 822 } 823 } else if (isIntersecting(mExpandedViewContainer, x, y)) { 824 return mExpandedViewContainer; 825 } 826 // Outside parts of view we care about. 827 return null; 828 } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) { 829 return mFlyout; 830 } 831 832 // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack. 833 return this; 834 } 835 getFlyoutView()836 View getFlyoutView() { 837 return mFlyout; 838 } 839 840 /** 841 * Collapses the stack of bubbles. 842 * <p> 843 * Must be called from the main thread. 844 * 845 * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)} 846 */ 847 @Deprecated 848 @MainThread collapseStack()849 void collapseStack() { 850 if (DEBUG) { 851 Log.d(TAG, "collapseStack()"); 852 } 853 mBubbleData.setExpanded(false); 854 } 855 856 /** 857 * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)} 858 */ 859 @Deprecated 860 @MainThread collapseStack(Runnable endRunnable)861 void collapseStack(Runnable endRunnable) { 862 if (DEBUG) { 863 Log.d(TAG, "collapseStack(endRunnable)"); 864 } 865 collapseStack(); 866 // TODO - use the runnable at end of animation 867 endRunnable.run(); 868 } 869 870 /** 871 * Expands the stack of bubbles. 872 * <p> 873 * Must be called from the main thread. 874 * 875 * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)} 876 */ 877 @Deprecated 878 @MainThread expandStack()879 void expandStack() { 880 if (DEBUG) { 881 Log.d(TAG, "expandStack()"); 882 } 883 mBubbleData.setExpanded(true); 884 } 885 886 /** 887 * Tell the stack to animate to collapsed or expanded state. 888 */ animateExpansion(boolean shouldExpand)889 private void animateExpansion(boolean shouldExpand) { 890 if (DEBUG) { 891 Log.d(TAG, "animateExpansion: shouldExpand=" + shouldExpand); 892 } 893 if (mIsExpanded != shouldExpand) { 894 hideFlyoutImmediate(); 895 896 mIsExpanded = shouldExpand; 897 updateExpandedBubble(); 898 applyCurrentState(); 899 900 mIsExpansionAnimating = true; 901 902 Runnable updateAfter = () -> { 903 applyCurrentState(); 904 mIsExpansionAnimating = false; 905 requestUpdate(); 906 }; 907 908 if (shouldExpand) { 909 mBubbleContainer.setActiveController(mExpandedAnimationController); 910 mExpandedAnimationController.expandFromStack(() -> { 911 updatePointerPosition(); 912 updateAfter.run(); 913 } /* after */); 914 } else { 915 mBubbleContainer.cancelAllAnimations(); 916 mExpandedAnimationController.collapseBackToStack( 917 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(), 918 () -> { 919 mBubbleContainer.setActiveController(mStackAnimationController); 920 updateAfter.run(); 921 }); 922 } 923 924 final float xStart = 925 mStackAnimationController.getStackPosition().x < getWidth() / 2 926 ? -mExpandedAnimateXDistance 927 : mExpandedAnimateXDistance; 928 929 final float yStart = Math.min( 930 mStackAnimationController.getStackPosition().y, 931 mExpandedAnimateYDistance); 932 final float yDest = getYPositionForExpandedView(); 933 934 if (shouldExpand) { 935 mExpandedViewContainer.setTranslationX(xStart); 936 mExpandedViewContainer.setTranslationY(yStart); 937 mExpandedViewContainer.setAlpha(0f); 938 } 939 940 mExpandedViewXAnim.animateToFinalPosition(shouldExpand ? 0f : xStart); 941 mExpandedViewYAnim.animateToFinalPosition(shouldExpand ? yDest : yStart); 942 mExpandedViewContainer.animate() 943 .setDuration(100) 944 .alpha(shouldExpand ? 1f : 0f); 945 } 946 } 947 948 private void notifyExpansionChanged(NotificationEntry entry, boolean expanded) { 949 if (mExpandListener != null) { 950 mExpandListener.onBubbleExpandChanged(expanded, entry != null ? entry.key : null); 951 } 952 } 953 954 /** Return the BubbleView at the given index from the bubble container. */ 955 public BubbleView getBubbleAt(int i) { 956 return mBubbleContainer.getChildCount() > i 957 ? (BubbleView) mBubbleContainer.getChildAt(i) 958 : null; 959 } 960 961 /** Moves the bubbles out of the way if they're going to be over the keyboard. */ onImeVisibilityChanged(boolean visible, int height)962 public void onImeVisibilityChanged(boolean visible, int height) { 963 mStackAnimationController.setImeHeight(height + mImeOffset); 964 965 if (!mIsExpanded) { 966 mStackAnimationController.animateForImeVisibility(visible); 967 } 968 } 969 970 /** Called when a drag operation on an individual bubble has started. */ onBubbleDragStart(View bubble)971 public void onBubbleDragStart(View bubble) { 972 if (DEBUG) { 973 Log.d(TAG, "onBubbleDragStart: bubble=" + bubble); 974 } 975 mExpandedAnimationController.prepareForBubbleDrag(bubble); 976 } 977 978 /** Called with the coordinates to which an individual bubble has been dragged. */ onBubbleDragged(View bubble, float x, float y)979 public void onBubbleDragged(View bubble, float x, float y) { 980 if (!mIsExpanded || mIsExpansionAnimating) { 981 return; 982 } 983 984 mExpandedAnimationController.dragBubbleOut(bubble, x, y); 985 springInDismissTarget(); 986 } 987 988 /** Called when a drag operation on an individual bubble has finished. */ onBubbleDragFinish( View bubble, float x, float y, float velX, float velY)989 public void onBubbleDragFinish( 990 View bubble, float x, float y, float velX, float velY) { 991 if (DEBUG) { 992 Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble); 993 } 994 995 if (!mIsExpanded || mIsExpansionAnimating) { 996 return; 997 } 998 999 mExpandedAnimationController.snapBubbleBack(bubble, velX, velY); 1000 springOutDismissTargetAndHideCircle(); 1001 } 1002 onDragStart()1003 void onDragStart() { 1004 if (DEBUG) { 1005 Log.d(TAG, "onDragStart()"); 1006 } 1007 if (mIsExpanded || mIsExpansionAnimating) { 1008 return; 1009 } 1010 1011 mStackAnimationController.cancelStackPositionAnimations(); 1012 mBubbleContainer.setActiveController(mStackAnimationController); 1013 hideFlyoutImmediate(); 1014 1015 mDraggingInDismissTarget = false; 1016 } 1017 onDragged(float x, float y)1018 void onDragged(float x, float y) { 1019 if (mIsExpanded || mIsExpansionAnimating) { 1020 return; 1021 } 1022 1023 springInDismissTarget(); 1024 mStackAnimationController.moveStackFromTouch(x, y); 1025 } 1026 onDragFinish(float x, float y, float velX, float velY)1027 void onDragFinish(float x, float y, float velX, float velY) { 1028 if (DEBUG) { 1029 Log.d(TAG, "onDragFinish"); 1030 } 1031 1032 if (mIsExpanded || mIsExpansionAnimating) { 1033 return; 1034 } 1035 1036 final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY); 1037 logBubbleEvent(null /* no bubble associated with bubble stack move */, 1038 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); 1039 1040 mStackOnLeftOrWillBe = newStackX <= 0; 1041 updateBubbleShadowsAndDotPosition(true /* animate */); 1042 springOutDismissTargetAndHideCircle(); 1043 } 1044 onFlyoutDragStart()1045 void onFlyoutDragStart() { 1046 mFlyout.removeCallbacks(mHideFlyout); 1047 } 1048 onFlyoutDragged(float deltaX)1049 void onFlyoutDragged(float deltaX) { 1050 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 1051 mFlyoutDragDeltaX = deltaX; 1052 1053 final float collapsePercent = 1054 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); 1055 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); 1056 1057 // Calculate how to translate the flyout if it has been dragged too far in etiher direction. 1058 float overscrollTranslation = 0f; 1059 if (collapsePercent < 0f || collapsePercent > 1f) { 1060 // Whether we are more than 100% transitioned to the dot. 1061 final boolean overscrollingPastDot = collapsePercent > 1f; 1062 1063 // Whether we are overscrolling physically to the left - this can either be pulling the 1064 // flyout away from the stack (if the stack is on the right) or pushing it to the left 1065 // after it has already become the dot. 1066 final boolean overscrollingLeft = 1067 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); 1068 1069 overscrollTranslation = 1070 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) 1071 * (overscrollingLeft ? -1 : 1) 1072 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR 1073 // Attenuate the smaller dot less than the larger flyout. 1074 / (overscrollingPastDot ? 2 : 1))); 1075 } 1076 1077 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); 1078 } 1079 1080 /** 1081 * Called when the flyout drag has finished, and returns true if the gesture successfully 1082 * dismissed the flyout. 1083 */ onFlyoutDragFinished(float deltaX, float velX)1084 void onFlyoutDragFinished(float deltaX, float velX) { 1085 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 1086 final boolean metRequiredVelocity = 1087 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; 1088 final boolean metRequiredDeltaX = 1089 onLeft 1090 ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS 1091 : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; 1092 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; 1093 final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling); 1094 1095 mFlyout.removeCallbacks(mHideFlyout); 1096 animateFlyoutCollapsed(shouldDismiss, velX); 1097 } 1098 1099 /** 1100 * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.) 1101 * is received. 1102 */ 1103 void onGestureStart() { 1104 mIsGestureInProgress = true; 1105 } 1106 1107 /** Called when a gesture is completed or cancelled. */ 1108 void onGestureFinished() { 1109 mIsGestureInProgress = false; 1110 1111 if (mIsExpanded) { 1112 mExpandedAnimationController.onGestureFinished(); 1113 } 1114 } 1115 1116 /** Prepares and starts the desaturate/darken animation on the bubble stack. */ 1117 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { 1118 mDesaturateAndDarkenTargetView = targetView; 1119 1120 if (desaturateAndDarken) { 1121 // Use the animated paint for the bubbles. 1122 mDesaturateAndDarkenTargetView.setLayerType( 1123 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint); 1124 mDesaturateAndDarkenAnimator.removeAllListeners(); 1125 mDesaturateAndDarkenAnimator.start(); 1126 } else { 1127 mDesaturateAndDarkenAnimator.removeAllListeners(); 1128 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() { 1129 @Override 1130 public void onAnimationEnd(Animator animation) { 1131 super.onAnimationEnd(animation); 1132 // Stop using the animated paint. 1133 resetDesaturationAndDarken(); 1134 } 1135 }); 1136 mDesaturateAndDarkenAnimator.reverse(); 1137 } 1138 } 1139 1140 private void resetDesaturationAndDarken() { 1141 mDesaturateAndDarkenAnimator.removeAllListeners(); 1142 mDesaturateAndDarkenAnimator.cancel(); 1143 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null); 1144 } 1145 1146 /** 1147 * Magnets the stack to the target, while also transforming the target to encircle the stack and 1148 * desaturating/darkening the bubbles. 1149 */ 1150 void animateMagnetToDismissTarget( 1151 View magnetView, boolean toTarget, float x, float y, float velX, float velY) { 1152 mDraggingInDismissTarget = toTarget; 1153 1154 if (toTarget) { 1155 // The Y-value for the bubble stack to be positioned in the center of the dismiss target 1156 final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f; 1157 1158 mAnimatingMagnet = true; 1159 1160 final Runnable afterMagnet = () -> { 1161 mAnimatingMagnet = false; 1162 if (mAfterMagnet != null) { 1163 mAfterMagnet.run(); 1164 } 1165 }; 1166 1167 if (magnetView == this) { 1168 mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet); 1169 animateDesaturateAndDarken(mBubbleContainer, true); 1170 } else { 1171 mExpandedAnimationController.magnetBubbleToDismiss( 1172 magnetView, velX, velY, destY, afterMagnet); 1173 1174 animateDesaturateAndDarken(magnetView, true); 1175 } 1176 1177 mDismissContainer.animateEncircleCenterWithX(true); 1178 1179 } else { 1180 mAnimatingMagnet = false; 1181 1182 if (magnetView == this) { 1183 mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY); 1184 animateDesaturateAndDarken(mBubbleContainer, false); 1185 } else { 1186 mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY); 1187 animateDesaturateAndDarken(magnetView, false); 1188 } 1189 1190 mDismissContainer.animateEncircleCenterWithX(false); 1191 } 1192 1193 mVibrator.vibrate(VibrationEffect.get(toTarget 1194 ? VibrationEffect.EFFECT_CLICK 1195 : VibrationEffect.EFFECT_TICK)); 1196 } 1197 1198 /** 1199 * Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack 1200 * using the 'implode' animation and animate out the target. 1201 */ magnetToStackIfNeededThenAnimateDismissal( View touchedView, float velX, float velY, Runnable after)1202 void magnetToStackIfNeededThenAnimateDismissal( 1203 View touchedView, float velX, float velY, Runnable after) { 1204 final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble(); 1205 final Runnable animateDismissal = () -> { 1206 mAfterMagnet = null; 1207 1208 mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); 1209 mDismissContainer.animateEncirclingCircleDisappearance(); 1210 1211 // 'Implode' the stack and then hide the dismiss target. 1212 if (touchedView == this) { 1213 mStackAnimationController.implodeStack( 1214 () -> { 1215 mAnimatingMagnet = false; 1216 mShowingDismiss = false; 1217 mDraggingInDismissTarget = false; 1218 after.run(); 1219 resetDesaturationAndDarken(); 1220 }); 1221 } else { 1222 mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> { 1223 mAnimatingMagnet = false; 1224 mShowingDismiss = false; 1225 mDraggingInDismissTarget = false; 1226 resetDesaturationAndDarken(); 1227 after.run(); 1228 }); 1229 } 1230 }; 1231 1232 if (mAnimatingMagnet) { 1233 // If the magnet animation is currently playing, dismiss the stack after it's done. This 1234 // happens if the stack is flung towards the target. 1235 mAfterMagnet = animateDismissal; 1236 } else if (mDraggingInDismissTarget) { 1237 // If we're in the dismiss target, but not animating, we already magneted - dismiss 1238 // immediately. 1239 animateDismissal.run(); 1240 } else { 1241 // Otherwise, we need to start the magnet animation and then dismiss afterward. 1242 animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY); 1243 mAfterMagnet = animateDismissal; 1244 } 1245 } 1246 1247 /** Animates in the dismiss target, including the gradient behind it. */ springInDismissTarget()1248 private void springInDismissTarget() { 1249 if (mShowingDismiss) { 1250 return; 1251 } 1252 1253 mShowingDismiss = true; 1254 1255 // Show the dismiss container and bring it to the front so the bubbles will go behind it. 1256 mDismissContainer.springIn(); 1257 mDismissContainer.bringToFront(); 1258 mDismissContainer.setZ(Short.MAX_VALUE - 1); 1259 } 1260 1261 /** 1262 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they 1263 * were dragged into the target and encircled. 1264 */ springOutDismissTargetAndHideCircle()1265 private void springOutDismissTargetAndHideCircle() { 1266 if (!mShowingDismiss) { 1267 return; 1268 } 1269 1270 mDismissContainer.springOut(); 1271 mShowingDismiss = false; 1272 } 1273 1274 /** Whether the location of the given MotionEvent is within the dismiss target area. */ isInDismissTarget(MotionEvent ev)1275 boolean isInDismissTarget(MotionEvent ev) { 1276 return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY()); 1277 } 1278 1279 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ animateFlyoutCollapsed(boolean collapsed, float velX)1280 private void animateFlyoutCollapsed(boolean collapsed, float velX) { 1281 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 1282 mFlyoutTransitionSpring 1283 .setStartValue(mFlyoutDragDeltaX) 1284 .setStartVelocity(velX) 1285 .animateToFinalPosition(collapsed 1286 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) 1287 : 0f); 1288 } 1289 1290 /** 1291 * Calculates how large the expanded view of the bubble can be. This takes into account the 1292 * y position when the bubbles are expanded as well as the bounds of the dismiss target. 1293 */ getMaxExpandedHeight()1294 int getMaxExpandedHeight() { 1295 int expandedY = (int) mExpandedAnimationController.getExpandedY(); 1296 // PIP dismiss view uses FLAG_LAYOUT_IN_SCREEN so we need to subtract the bottom inset 1297 int pipDismissHeight = mPipDismissHeight - getBottomInset(); 1298 return mDisplaySize.y - expandedY - mBubbleSize - pipDismissHeight; 1299 } 1300 1301 /** 1302 * Calculates the y position of the expanded view when it is expanded. 1303 */ getYPositionForExpandedView()1304 float getYPositionForExpandedView() { 1305 return getStatusBarHeight() + mBubbleSize + mBubblePadding + mPointerHeight; 1306 } 1307 1308 /** 1309 * Called when the height of the currently expanded view has changed (not via an 1310 * update to the bubble's desired height but for some other reason, e.g. permission view 1311 * goes away). 1312 */ onExpandedHeightChanged()1313 void onExpandedHeightChanged() { 1314 if (mIsExpanded) { 1315 requestUpdate(); 1316 } 1317 } 1318 1319 /** Sets whether all bubbles in the stack should not show the 'new' dot. */ setSuppressNewDot(boolean suppressNewDot)1320 void setSuppressNewDot(boolean suppressNewDot) { 1321 mSuppressNewDot = suppressNewDot; 1322 1323 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { 1324 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); 1325 bv.setSuppressDot(suppressNewDot, true /* animate */); 1326 } 1327 } 1328 1329 /** 1330 * Sets whether the flyout should not appear, even if the notif otherwise would generate one. 1331 */ setSuppressFlyout(boolean suppressFlyout)1332 void setSuppressFlyout(boolean suppressFlyout) { 1333 mSuppressFlyout = suppressFlyout; 1334 } 1335 1336 /** 1337 * Callback to run after the flyout hides. Also called if a new flyout is shown before the 1338 * previous one animates out. 1339 */ 1340 private Runnable mAfterFlyoutHides; 1341 1342 /** 1343 * Animates in the flyout for the given bubble, if available, and then hides it after some time. 1344 */ 1345 @VisibleForTesting animateInFlyoutForBubble(Bubble bubble)1346 void animateInFlyoutForBubble(Bubble bubble) { 1347 final CharSequence updateMessage = bubble.entry.getUpdateMessage(getContext()); 1348 1349 // Show the message if one exists, and we're not expanded or animating expansion. 1350 if (updateMessage != null 1351 && !isExpanded() 1352 && !mIsExpansionAnimating 1353 && !mIsGestureInProgress 1354 && !mSuppressFlyout) { 1355 if (bubble.iconView != null) { 1356 // Temporarily suppress the dot while the flyout is visible. 1357 bubble.iconView.setSuppressDot( 1358 true /* suppressDot */, false /* animate */); 1359 1360 mFlyoutDragDeltaX = 0f; 1361 mFlyout.setAlpha(0f); 1362 1363 if (mAfterFlyoutHides != null) { 1364 mAfterFlyoutHides.run(); 1365 } 1366 1367 mAfterFlyoutHides = () -> { 1368 if (bubble.iconView == null) { 1369 return; 1370 } 1371 1372 // If we're going to suppress the dot, make it visible first so it'll 1373 // visibly animate away. 1374 if (mSuppressNewDot) { 1375 bubble.iconView.setSuppressDot( 1376 false /* suppressDot */, false /* animate */); 1377 } 1378 1379 // Reset dot suppression. If we're not suppressing due to DND, then 1380 // stop suppressing it with no animation (since the flyout has 1381 // transformed into the dot). If we are suppressing due to DND, animate 1382 // it away. 1383 bubble.iconView.setSuppressDot( 1384 mSuppressNewDot /* suppressDot */, 1385 mSuppressNewDot /* animate */); 1386 }; 1387 1388 // Post in case layout isn't complete and getWidth returns 0. 1389 post(() -> { 1390 // An auto-expanding bubble could have been posted during the time it takes to 1391 // layout. 1392 if (isExpanded()) { 1393 return; 1394 } 1395 1396 mFlyout.showFlyout( 1397 updateMessage, mStackAnimationController.getStackPosition(), getWidth(), 1398 mStackAnimationController.isStackOnLeftSide(), 1399 bubble.iconView.getBadgeColor(), mAfterFlyoutHides); 1400 }); 1401 } 1402 1403 mFlyout.removeCallbacks(mHideFlyout); 1404 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 1405 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); 1406 } 1407 } 1408 1409 /** Hide the flyout immediately and cancel any pending hide runnables. */ hideFlyoutImmediate()1410 private void hideFlyoutImmediate() { 1411 if (mAfterFlyoutHides != null) { 1412 mAfterFlyoutHides.run(); 1413 } 1414 1415 mFlyout.removeCallbacks(mHideFlyout); 1416 mFlyout.hideFlyout(); 1417 } 1418 1419 @Override getBoundsOnScreen(Rect outRect)1420 public void getBoundsOnScreen(Rect outRect) { 1421 if (!mIsExpanded) { 1422 if (mBubbleContainer.getChildCount() > 0) { 1423 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); 1424 } 1425 } else { 1426 mBubbleContainer.getBoundsOnScreen(outRect); 1427 } 1428 1429 if (mFlyout.getVisibility() == View.VISIBLE) { 1430 final Rect flyoutBounds = new Rect(); 1431 mFlyout.getBoundsOnScreen(flyoutBounds); 1432 outRect.union(flyoutBounds); 1433 } 1434 } 1435 getStatusBarHeight()1436 private int getStatusBarHeight() { 1437 if (getRootWindowInsets() != null) { 1438 WindowInsets insets = getRootWindowInsets(); 1439 return Math.max( 1440 mStatusBarHeight, 1441 insets.getDisplayCutout() != null 1442 ? insets.getDisplayCutout().getSafeInsetTop() 1443 : 0); 1444 } 1445 1446 return 0; 1447 } 1448 getBottomInset()1449 private int getBottomInset() { 1450 if (getRootWindowInsets() != null) { 1451 WindowInsets insets = getRootWindowInsets(); 1452 return insets.getSystemWindowInsetBottom(); 1453 } 1454 return 0; 1455 } 1456 isIntersecting(View view, float x, float y)1457 private boolean isIntersecting(View view, float x, float y) { 1458 mTempLoc = view.getLocationOnScreen(); 1459 mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(), 1460 mTempLoc[1] + view.getHeight()); 1461 return mTempRect.contains(x, y); 1462 } 1463 requestUpdate()1464 private void requestUpdate() { 1465 if (mViewUpdatedRequested || mIsExpansionAnimating) { 1466 return; 1467 } 1468 mViewUpdatedRequested = true; 1469 getViewTreeObserver().addOnPreDrawListener(mViewUpdater); 1470 invalidate(); 1471 } 1472 updateExpandedBubble()1473 private void updateExpandedBubble() { 1474 if (DEBUG) { 1475 Log.d(TAG, "updateExpandedBubble()"); 1476 } 1477 mExpandedViewContainer.removeAllViews(); 1478 if (mExpandedBubble != null && mIsExpanded) { 1479 mExpandedViewContainer.addView(mExpandedBubble.expandedView); 1480 mExpandedBubble.expandedView.populateExpandedView(); 1481 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); 1482 mExpandedViewContainer.setAlpha(1.0f); 1483 } 1484 } 1485 applyCurrentState()1486 private void applyCurrentState() { 1487 if (DEBUG) { 1488 Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded); 1489 } 1490 1491 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); 1492 if (mIsExpanded) { 1493 // First update the view so that it calculates a new height (ensuring the y position 1494 // calculation is correct) 1495 mExpandedBubble.expandedView.updateView(); 1496 final float y = getYPositionForExpandedView(); 1497 if (!mExpandedViewYAnim.isRunning()) { 1498 // We're not animating so set the value 1499 mExpandedViewContainer.setTranslationY(y); 1500 mExpandedBubble.expandedView.updateView(); 1501 } else { 1502 // We are animating so update the value; there is an end listener on the animator 1503 // that will ensure expandedeView.updateView gets called. 1504 mExpandedViewYAnim.animateToFinalPosition(y); 1505 } 1506 } 1507 1508 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 1509 updateBubbleShadowsAndDotPosition(false); 1510 } 1511 1512 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */ updateBubbleShadowsAndDotPosition(boolean animate)1513 private void updateBubbleShadowsAndDotPosition(boolean animate) { 1514 int bubbsCount = mBubbleContainer.getChildCount(); 1515 for (int i = 0; i < bubbsCount; i++) { 1516 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); 1517 bv.updateDotVisibility(true /* animate */); 1518 bv.setZ((BubbleController.MAX_BUBBLES 1519 * getResources().getDimensionPixelSize(R.dimen.bubble_elevation)) - i); 1520 1521 // Draw the shadow around the circle inscribed within the bubble's bounds. This 1522 // (intentionally) does not draw a shadow behind the update dot, which should be drawing 1523 // its own shadow since it's on a different (higher) plane. 1524 bv.setOutlineProvider(new ViewOutlineProvider() { 1525 @Override 1526 public void getOutline(View view, Outline outline) { 1527 outline.setOval(0, 0, mBubbleSize, mBubbleSize); 1528 } 1529 }); 1530 bv.setClipToOutline(false); 1531 1532 // If the dot is on the left, and so is the stack, we need to change the dot position. 1533 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) { 1534 bv.setDotPosition(!mStackOnLeftOrWillBe, animate); 1535 } 1536 } 1537 } 1538 updatePointerPosition()1539 private void updatePointerPosition() { 1540 if (DEBUG) { 1541 Log.d(TAG, "updatePointerPosition()"); 1542 } 1543 1544 Bubble expandedBubble = getExpandedBubble(); 1545 if (expandedBubble == null) { 1546 return; 1547 } 1548 1549 int index = getBubbleIndex(expandedBubble); 1550 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index); 1551 float halfBubble = mBubbleSize / 2f; 1552 1553 // Bubbles live in expanded view container (x includes expanded view padding). 1554 // Pointer lives in expanded view, which has padding (x does not include padding). 1555 // Remove padding when deriving pointer location from bubbles. 1556 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble - mExpandedViewPadding; 1557 1558 expandedBubble.expandedView.setPointerPosition(bubbleCenter); 1559 } 1560 1561 /** 1562 * @return the number of bubbles in the stack view. 1563 */ getBubbleCount()1564 public int getBubbleCount() { 1565 return mBubbleContainer.getChildCount(); 1566 } 1567 1568 /** 1569 * Finds the bubble index within the stack. 1570 * 1571 * @param bubble the bubble to look up. 1572 * @return the index of the bubble view within the bubble stack. The range of the position 1573 * is between 0 and the bubble count minus 1. 1574 */ getBubbleIndex(@ullable Bubble bubble)1575 int getBubbleIndex(@Nullable Bubble bubble) { 1576 if (bubble == null) { 1577 return 0; 1578 } 1579 return mBubbleContainer.indexOfChild(bubble.iconView); 1580 } 1581 1582 /** 1583 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places. 1584 */ getNormalizedXPosition()1585 public float getNormalizedXPosition() { 1586 return new BigDecimal(getStackPosition().x / mDisplaySize.x) 1587 .setScale(4, RoundingMode.CEILING.HALF_UP) 1588 .floatValue(); 1589 } 1590 1591 /** 1592 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places. 1593 */ getNormalizedYPosition()1594 public float getNormalizedYPosition() { 1595 return new BigDecimal(getStackPosition().y / mDisplaySize.y) 1596 .setScale(4, RoundingMode.CEILING.HALF_UP) 1597 .floatValue(); 1598 } 1599 getStackPosition()1600 public PointF getStackPosition() { 1601 return mStackAnimationController.getStackPosition(); 1602 } 1603 1604 /** 1605 * Logs the bubble UI event. 1606 * 1607 * @param bubble the bubble that is being interacted on. Null value indicates that 1608 * the user interaction is not specific to one bubble. 1609 * @param action the user interaction enum. 1610 */ logBubbleEvent(@ullable Bubble bubble, int action)1611 private void logBubbleEvent(@Nullable Bubble bubble, int action) { 1612 if (bubble == null || bubble.entry == null 1613 || bubble.entry.notification == null) { 1614 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED, 1615 null /* package name */, 1616 null /* notification channel */, 1617 0 /* notification ID */, 1618 0 /* bubble position */, 1619 getBubbleCount(), 1620 action, 1621 getNormalizedXPosition(), 1622 getNormalizedYPosition(), 1623 false /* unread bubble */, 1624 false /* on-going bubble */, 1625 false /* foreground bubble */); 1626 } else { 1627 StatusBarNotification notification = bubble.entry.notification; 1628 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED, 1629 notification.getPackageName(), 1630 notification.getNotification().getChannelId(), 1631 notification.getId(), 1632 getBubbleIndex(bubble), 1633 getBubbleCount(), 1634 action, 1635 getNormalizedXPosition(), 1636 getNormalizedYPosition(), 1637 bubble.entry.showInShadeWhenBubble(), 1638 bubble.entry.isForegroundService(), 1639 BubbleController.isForegroundApp(mContext, notification.getPackageName())); 1640 } 1641 } 1642 1643 /** 1644 * Called when a back gesture should be directed to the Bubbles stack. When expanded, 1645 * a back key down/up event pair is forwarded to the bubble Activity. 1646 */ performBackPressIfNeeded()1647 boolean performBackPressIfNeeded() { 1648 if (!isExpanded()) { 1649 return false; 1650 } 1651 return mExpandedBubble.expandedView.performBackPressIfNeeded(); 1652 } 1653 1654 /** For debugging only */ getBubblesOnScreen()1655 List<Bubble> getBubblesOnScreen() { 1656 List<Bubble> bubbles = new ArrayList<>(); 1657 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { 1658 View child = mBubbleContainer.getChildAt(i); 1659 if (child instanceof BubbleView) { 1660 String key = ((BubbleView) child).getKey(); 1661 Bubble bubble = mBubbleData.getBubbleWithKey(key); 1662 bubbles.add(bubble); 1663 } 1664 } 1665 return bubbles; 1666 } 1667 } 1668