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 static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; 23 import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION; 24 import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION; 25 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; 26 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION; 27 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; 28 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 29 30 import android.animation.Animator; 31 import android.animation.AnimatorListenerAdapter; 32 import android.animation.ValueAnimator; 33 import android.annotation.SuppressLint; 34 import android.app.ActivityView; 35 import android.content.ContentResolver; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.res.Configuration; 39 import android.content.res.Resources; 40 import android.content.res.TypedArray; 41 import android.graphics.Color; 42 import android.graphics.ColorMatrix; 43 import android.graphics.ColorMatrixColorFilter; 44 import android.graphics.Outline; 45 import android.graphics.Paint; 46 import android.graphics.Point; 47 import android.graphics.PointF; 48 import android.graphics.Rect; 49 import android.graphics.RectF; 50 import android.graphics.Region; 51 import android.graphics.drawable.TransitionDrawable; 52 import android.os.Bundle; 53 import android.os.Handler; 54 import android.provider.Settings; 55 import android.util.Log; 56 import android.view.Choreographer; 57 import android.view.DisplayCutout; 58 import android.view.Gravity; 59 import android.view.LayoutInflater; 60 import android.view.MotionEvent; 61 import android.view.SurfaceControl; 62 import android.view.SurfaceView; 63 import android.view.View; 64 import android.view.ViewGroup; 65 import android.view.ViewOutlineProvider; 66 import android.view.ViewTreeObserver; 67 import android.view.WindowInsets; 68 import android.view.WindowManager; 69 import android.view.accessibility.AccessibilityNodeInfo; 70 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 71 import android.view.animation.AccelerateDecelerateInterpolator; 72 import android.widget.FrameLayout; 73 import android.widget.ImageView; 74 import android.widget.LinearLayout; 75 import android.widget.TextView; 76 77 import androidx.annotation.MainThread; 78 import androidx.annotation.NonNull; 79 import androidx.annotation.Nullable; 80 import androidx.dynamicanimation.animation.DynamicAnimation; 81 import androidx.dynamicanimation.animation.FloatPropertyCompat; 82 import androidx.dynamicanimation.animation.SpringAnimation; 83 import androidx.dynamicanimation.animation.SpringForce; 84 85 import com.android.internal.annotations.VisibleForTesting; 86 import com.android.internal.util.ContrastColorUtil; 87 import com.android.systemui.Interpolators; 88 import com.android.systemui.Prefs; 89 import com.android.systemui.R; 90 import com.android.systemui.bubbles.animation.AnimatableScaleMatrix; 91 import com.android.systemui.bubbles.animation.ExpandedAnimationController; 92 import com.android.systemui.bubbles.animation.PhysicsAnimationLayout; 93 import com.android.systemui.bubbles.animation.StackAnimationController; 94 import com.android.systemui.model.SysUiState; 95 import com.android.systemui.shared.system.QuickStepContract; 96 import com.android.systemui.shared.system.SysUiStatsLog; 97 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; 98 import com.android.systemui.util.DismissCircleView; 99 import com.android.systemui.util.FloatingContentCoordinator; 100 import com.android.systemui.util.RelativeTouchListener; 101 import com.android.systemui.util.animation.PhysicsAnimator; 102 import com.android.systemui.util.magnetictarget.MagnetizedObject; 103 104 import java.io.FileDescriptor; 105 import java.io.PrintWriter; 106 import java.math.BigDecimal; 107 import java.math.RoundingMode; 108 import java.util.ArrayList; 109 import java.util.Collections; 110 import java.util.List; 111 import java.util.function.Consumer; 112 113 /** 114 * Renders bubbles in a stack and handles animating expanded and collapsed states. 115 */ 116 public class BubbleStackView extends FrameLayout 117 implements ViewTreeObserver.OnComputeInternalInsetsListener { 118 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; 119 120 /** Animation durations for bubble stack user education views. **/ 121 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200; 122 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40; 123 124 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ 125 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; 126 127 /** Velocity required to dismiss the flyout via drag. */ 128 private static final float FLYOUT_DISMISS_VELOCITY = 2000f; 129 130 /** 131 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel 132 * for every 8 pixels overscrolled). 133 */ 134 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; 135 136 /** Duration of the flyout alpha animations. */ 137 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; 138 139 /** Percent to darken the bubbles when they're in the dismiss target. */ 140 private static final float DARKEN_PERCENT = 0.3f; 141 142 /** Duration of the dismiss scrim fading in/out. */ 143 private static final int DISMISS_TRANSITION_DURATION_MS = 200; 144 145 /** How long to wait, in milliseconds, before hiding the flyout. */ 146 @VisibleForTesting 147 static final int FLYOUT_HIDE_AFTER = 5000; 148 149 /** 150 * How long to wait to animate the stack temporarily invisible after a drag/flyout hide 151 * animation ends, if we are in fact temporarily invisible. 152 */ 153 private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000; 154 155 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG = 156 new PhysicsAnimator.SpringConfig( 157 StackAnimationController.IME_ANIMATION_STIFFNESS, 158 StackAnimationController.DEFAULT_BOUNCINESS); 159 160 private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = 161 new PhysicsAnimator.SpringConfig(300f, 0.9f); 162 163 private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig = 164 new PhysicsAnimator.SpringConfig(900f, 1f); 165 166 private final PhysicsAnimator.SpringConfig mTranslateSpringConfig = 167 new PhysicsAnimator.SpringConfig( 168 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); 169 170 /** 171 * Handler to use for all delayed animations - this way, we can easily cancel them before 172 * starting a new animation. 173 */ 174 private final Handler mDelayedAnimationHandler = new Handler(); 175 176 /** 177 * Interface to synchronize {@link View} state and the screen. 178 * 179 * {@hide} 180 */ 181 interface SurfaceSynchronizer { 182 /** 183 * Wait until requested change on a {@link View} is reflected on the screen. 184 * 185 * @param callback callback to run after the change is reflected on the screen. 186 */ syncSurfaceAndRun(Runnable callback)187 void syncSurfaceAndRun(Runnable callback); 188 } 189 190 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = 191 new SurfaceSynchronizer() { 192 @Override 193 public void syncSurfaceAndRun(Runnable callback) { 194 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { 195 // Just wait 2 frames. There is no guarantee, but this is usually enough time that 196 // the requested change is reflected on the screen. 197 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and 198 // surfaces, rewrite this logic with them. 199 private int mFrameWait = 2; 200 201 @Override 202 public void doFrame(long frameTimeNanos) { 203 if (--mFrameWait > 0) { 204 Choreographer.getInstance().postFrameCallback(this); 205 } else { 206 callback.run(); 207 } 208 } 209 }); 210 } 211 }; 212 213 private Point mDisplaySize; 214 215 private final BubbleData mBubbleData; 216 217 private final ValueAnimator mDesaturateAndDarkenAnimator; 218 private final Paint mDesaturateAndDarkenPaint = new Paint(); 219 220 private PhysicsAnimationLayout mBubbleContainer; 221 private StackAnimationController mStackAnimationController; 222 private ExpandedAnimationController mExpandedAnimationController; 223 224 private FrameLayout mExpandedViewContainer; 225 226 /** Matrix used to scale the expanded view container with a given pivot point. */ 227 private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix(); 228 229 /** 230 * SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate 231 * between bubble activities without needing both to be alive at the same time. 232 */ 233 private SurfaceView mAnimatingOutSurfaceView; 234 235 /** Container for the animating-out SurfaceView. */ 236 private FrameLayout mAnimatingOutSurfaceContainer; 237 238 /** 239 * Buffer containing a screenshot of the animating-out bubble. This is drawn into the 240 * SurfaceView during animations. 241 */ 242 private SurfaceControl.ScreenshotGraphicBuffer mAnimatingOutBubbleBuffer; 243 244 private BubbleFlyoutView mFlyout; 245 /** Runnable that fades out the flyout and then sets it to GONE. */ 246 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); 247 /** 248 * Callback to run after the flyout hides. Also called if a new flyout is shown before the 249 * previous one animates out. 250 */ 251 private Runnable mAfterFlyoutHidden; 252 /** 253 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout 254 * once it collapses. 255 */ 256 @Nullable 257 private Bubble mBubbleToExpandAfterFlyoutCollapse = null; 258 259 /** Layout change listener that moves the stack to the nearest valid position on rotation. */ 260 private OnLayoutChangeListener mOrientationChangedListener; 261 262 @Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation; 263 264 private int mMaxBubbles; 265 private int mBubbleSize; 266 private int mBubbleElevation; 267 private int mBubblePaddingTop; 268 private int mBubbleTouchPadding; 269 private int mExpandedViewPadding; 270 private int mCornerRadius; 271 private int mStatusBarHeight; 272 private int mImeOffset; 273 @Nullable private BubbleViewProvider mExpandedBubble; 274 private boolean mIsExpanded; 275 276 /** Whether the stack is currently on the left side of the screen, or animating there. */ 277 private boolean mStackOnLeftOrWillBe = true; 278 279 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ 280 private boolean mIsGestureInProgress = false; 281 282 /** Whether or not the stack is temporarily invisible off the side of the screen. */ 283 private boolean mTemporarilyInvisible = false; 284 285 /** Whether we're in the middle of dragging the stack around by touch. */ 286 private boolean mIsDraggingStack = false; 287 288 /** 289 * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore 290 * touches from other pointer indices. 291 */ 292 private int mPointerIndexDown = -1; 293 294 /** Description of current animation controller state. */ dump(FileDescriptor fd, PrintWriter pw, String[] args)295 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 296 pw.println("Stack view state:"); 297 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); 298 pw.print(" showingDismiss: "); pw.println(mShowingDismiss); 299 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); 300 pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility()); 301 pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha()); 302 pw.print(" expandedContainerMatrix: "); 303 pw.println(mExpandedViewContainer.getAnimationMatrix()); 304 305 mStackAnimationController.dump(fd, pw, args); 306 mExpandedAnimationController.dump(fd, pw, args); 307 308 if (mExpandedBubble != null) { 309 pw.println("Expanded bubble state:"); 310 pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); 311 312 final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView(); 313 314 if (expandedView != null) { 315 pw.println(" expandedViewVis: " + expandedView.getVisibility()); 316 pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); 317 pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); 318 319 final ActivityView av = expandedView.getActivityView(); 320 321 if (av != null) { 322 pw.println(" activityViewVis: " + av.getVisibility()); 323 pw.println(" activityViewAlpha: " + av.getAlpha()); 324 } else { 325 pw.println(" activityView is null"); 326 } 327 } else { 328 pw.println("Expanded bubble view state: expanded bubble view is null"); 329 } 330 } else { 331 pw.println("Expanded bubble state: expanded bubble is null"); 332 } 333 } 334 335 private BubbleController.BubbleExpandListener mExpandListener; 336 337 /** Callback to run when we want to unbubble the given notification's conversation. */ 338 private Consumer<String> mUnbubbleConversationCallback; 339 340 private SysUiState mSysUiState; 341 342 private boolean mViewUpdatedRequested = false; 343 private boolean mIsExpansionAnimating = false; 344 private boolean mIsBubbleSwitchAnimating = false; 345 private boolean mShowingDismiss = false; 346 347 /** The view to desaturate/darken when magneted to the dismiss target. */ 348 @Nullable private View mDesaturateAndDarkenTargetView; 349 350 private LayoutInflater mInflater; 351 352 private Rect mTempRect = new Rect(); 353 354 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); 355 356 private ViewTreeObserver.OnPreDrawListener mViewUpdater = 357 new ViewTreeObserver.OnPreDrawListener() { 358 @Override 359 public boolean onPreDraw() { 360 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 361 updateExpandedView(); 362 mViewUpdatedRequested = false; 363 return true; 364 } 365 }; 366 367 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = 368 this::updateSystemGestureExcludeRects; 369 370 /** Float property that 'drags' the flyout. */ 371 private final FloatPropertyCompat mFlyoutCollapseProperty = 372 new FloatPropertyCompat("FlyoutCollapseSpring") { 373 @Override 374 public float getValue(Object o) { 375 return mFlyoutDragDeltaX; 376 } 377 378 @Override 379 public void setValue(Object o, float v) { 380 setFlyoutStateForDragLength(v); 381 } 382 }; 383 384 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ 385 private final SpringAnimation mFlyoutTransitionSpring = 386 new SpringAnimation(this, mFlyoutCollapseProperty); 387 388 /** Distance the flyout has been dragged in the X axis. */ 389 private float mFlyoutDragDeltaX = 0f; 390 391 /** 392 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. 393 */ 394 private Runnable mAnimateInFlyout; 395 396 /** 397 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides 398 * it immediately. 399 */ 400 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = 401 (dynamicAnimation, b, v, v1) -> { 402 if (mFlyoutDragDeltaX == 0) { 403 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 404 } else { 405 mFlyout.hideFlyout(); 406 } 407 }; 408 409 @NonNull 410 private final SurfaceSynchronizer mSurfaceSynchronizer; 411 412 /** 413 * Callback to run when the IME visibility changes - BubbleController uses this to update the 414 * Bubbles window focusability flags with the WindowManager. 415 */ 416 public final Consumer<Boolean> mOnImeVisibilityChanged; 417 418 /** 419 * Callback to run to ask BubbleController to hide the current IME. 420 */ 421 private final Runnable mHideCurrentInputMethodCallback; 422 423 /** 424 * The currently magnetized object, which is being dragged and will be attracted to the magnetic 425 * dismiss target. 426 * 427 * This is either the stack itself, or an individual bubble. 428 */ 429 private MagnetizedObject<?> mMagnetizedObject; 430 431 /** 432 * The MagneticTarget instance for our circular dismiss view. This is added to the 433 * MagnetizedObject instances for the stack and any dragged-out bubbles. 434 */ 435 private MagnetizedObject.MagneticTarget mMagneticTarget; 436 437 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */ 438 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener = 439 new MagnetizedObject.MagnetListener() { 440 @Override 441 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { 442 if (mExpandedAnimationController.getDraggedOutBubble() == null) { 443 return; 444 } 445 446 animateDesaturateAndDarken( 447 mExpandedAnimationController.getDraggedOutBubble(), true); 448 } 449 450 @Override 451 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 452 float velX, float velY, boolean wasFlungOut) { 453 if (mExpandedAnimationController.getDraggedOutBubble() == null) { 454 return; 455 } 456 457 animateDesaturateAndDarken( 458 mExpandedAnimationController.getDraggedOutBubble(), false); 459 460 if (wasFlungOut) { 461 mExpandedAnimationController.snapBubbleBack( 462 mExpandedAnimationController.getDraggedOutBubble(), velX, velY); 463 hideDismissTarget(); 464 } else { 465 mExpandedAnimationController.onUnstuckFromTarget(); 466 } 467 } 468 469 @Override 470 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 471 if (mExpandedAnimationController.getDraggedOutBubble() == null) { 472 return; 473 } 474 475 mExpandedAnimationController.dismissDraggedOutBubble( 476 mExpandedAnimationController.getDraggedOutBubble() /* bubble */, 477 mDismissTargetContainer.getHeight() /* translationYBy */, 478 BubbleStackView.this::dismissMagnetizedObject /* after */); 479 hideDismissTarget(); 480 } 481 }; 482 483 /** Magnet listener that handles animating and dismissing the entire stack. */ 484 private final MagnetizedObject.MagnetListener mStackMagnetListener = 485 new MagnetizedObject.MagnetListener() { 486 @Override 487 public void onStuckToTarget( 488 @NonNull MagnetizedObject.MagneticTarget target) { 489 animateDesaturateAndDarken(mBubbleContainer, true); 490 } 491 492 @Override 493 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 494 float velX, float velY, boolean wasFlungOut) { 495 animateDesaturateAndDarken(mBubbleContainer, false); 496 497 if (wasFlungOut) { 498 mStackAnimationController.flingStackThenSpringToEdge( 499 mStackAnimationController.getStackPosition().x, velX, velY); 500 hideDismissTarget(); 501 } else { 502 mStackAnimationController.onUnstuckFromTarget(); 503 } 504 } 505 506 @Override 507 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 508 mStackAnimationController.animateStackDismissal( 509 mDismissTargetContainer.getHeight() /* translationYBy */, 510 () -> { 511 resetDesaturationAndDarken(); 512 dismissMagnetizedObject(); 513 } 514 ); 515 516 hideDismissTarget(); 517 } 518 }; 519 520 /** 521 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack. 522 * When expanded, clicking a bubble either expands that bubble, or collapses the stack. 523 */ 524 private OnClickListener mBubbleClickListener = new OnClickListener() { 525 @Override 526 public void onClick(View view) { 527 mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging. 528 529 // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we 530 // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust 531 // the animations inflight. 532 if (mIsExpansionAnimating || mIsBubbleSwitchAnimating) { 533 return; 534 } 535 536 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view); 537 538 // If the bubble has since left us, ignore the click. 539 if (clickedBubble == null) { 540 return; 541 } 542 543 final boolean clickedBubbleIsCurrentlyExpandedBubble = 544 clickedBubble.getKey().equals(mExpandedBubble.getKey()); 545 546 if (isExpanded()) { 547 mExpandedAnimationController.onGestureFinished(); 548 } 549 550 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) { 551 if (clickedBubble != mBubbleData.getSelectedBubble()) { 552 // Select the clicked bubble. 553 mBubbleData.setSelectedBubble(clickedBubble); 554 } else { 555 // If the clicked bubble is the selected bubble (but not the expanded bubble), 556 // that means overflow was previously expanded. Set the selected bubble 557 // internally without going through BubbleData (which would ignore it since it's 558 // already selected). 559 setSelectedBubble(clickedBubble); 560 } 561 } else { 562 // Otherwise, we either tapped the stack (which means we're collapsed 563 // and should expand) or the currently selected bubble (we're expanded 564 // and should collapse). 565 if (!maybeShowStackUserEducation()) { 566 mBubbleData.setExpanded(!mBubbleData.isExpanded()); 567 } 568 } 569 } 570 }; 571 572 /** 573 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when 574 * collapsed), or individual bubbles (when expanded). 575 */ 576 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() { 577 578 @Override 579 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 580 // If we're expanding or collapsing, consume but ignore all touch events. 581 if (mIsExpansionAnimating) { 582 return true; 583 } 584 585 // If the manage menu is visible, just hide it. 586 if (mShowingManage) { 587 showManageMenu(false /* show */); 588 } 589 590 if (mBubbleData.isExpanded()) { 591 maybeShowManageEducation(false /* show */); 592 593 // If we're expanded, tell the animation controller to prepare to drag this bubble, 594 // dispatching to the individual bubble magnet listener. 595 mExpandedAnimationController.prepareForBubbleDrag( 596 v /* bubble */, 597 mMagneticTarget, 598 mIndividualBubbleMagnetListener); 599 600 hideCurrentInputMethod(); 601 602 // Save the magnetized individual bubble so we can dispatch touch events to it. 603 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); 604 } else { 605 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the 606 // animation controller, and hide the flyout. 607 mStackAnimationController.cancelStackPositionAnimations(); 608 mBubbleContainer.setActiveController(mStackAnimationController); 609 hideFlyoutImmediate(); 610 611 // Also, save the magnetized stack so we can dispatch touch events to it. 612 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget); 613 mMagnetizedObject.setMagnetListener(mStackMagnetListener); 614 615 mIsDraggingStack = true; 616 617 // Cancel animations to make the stack temporarily invisible, since we're now 618 // dragging it. 619 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 620 } 621 622 passEventToMagnetizedObject(ev); 623 624 // Bubbles are always interested in all touch events! 625 return true; 626 } 627 628 @Override 629 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 630 float viewInitialY, float dx, float dy) { 631 // If we're expanding or collapsing, ignore all touch events. 632 if (mIsExpansionAnimating) { 633 return; 634 } 635 636 // Show the dismiss target, if we haven't already. 637 springInDismissTargetMaybe(); 638 639 // First, see if the magnetized object consumes the event - if so, we shouldn't move the 640 // bubble since it's stuck to the target. 641 if (!passEventToMagnetizedObject(ev)) { 642 if (mBubbleData.isExpanded()) { 643 mExpandedAnimationController.dragBubbleOut( 644 v, viewInitialX + dx, viewInitialY + dy); 645 } else { 646 hideStackUserEducation(false /* fromExpansion */); 647 mStackAnimationController.moveStackFromTouch( 648 viewInitialX + dx, viewInitialY + dy); 649 } 650 } 651 } 652 653 @Override 654 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 655 float viewInitialY, float dx, float dy, float velX, float velY) { 656 // If we're expanding or collapsing, ignore all touch events. 657 if (mIsExpansionAnimating) { 658 return; 659 } 660 661 // First, see if the magnetized object consumes the event - if so, the bubble was 662 // released in the target or flung out of it, and we should ignore the event. 663 if (!passEventToMagnetizedObject(ev)) { 664 if (mBubbleData.isExpanded()) { 665 mExpandedAnimationController.snapBubbleBack(v, velX, velY); 666 } else { 667 // Fling the stack to the edge, and save whether or not it's going to end up on 668 // the left side of the screen. 669 mStackOnLeftOrWillBe = 670 mStackAnimationController.flingStackThenSpringToEdge( 671 viewInitialX + dx, velX, velY) <= 0; 672 673 updateBubbleZOrdersAndDotPosition(true /* animate */); 674 675 logBubbleEvent(null /* no bubble associated with bubble stack move */, 676 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); 677 } 678 679 hideDismissTarget(); 680 } 681 682 mIsDraggingStack = false; 683 684 // Hide the stack after a delay, if needed. 685 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 686 } 687 }; 688 689 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */ 690 private OnClickListener mFlyoutClickListener = new OnClickListener() { 691 @Override 692 public void onClick(View view) { 693 if (maybeShowStackUserEducation()) { 694 // If we're showing user education, don't open the bubble show the education first 695 mBubbleToExpandAfterFlyoutCollapse = null; 696 } else { 697 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); 698 } 699 700 mFlyout.removeCallbacks(mHideFlyout); 701 mHideFlyout.run(); 702 } 703 }; 704 705 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */ 706 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() { 707 708 @Override 709 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 710 mFlyout.removeCallbacks(mHideFlyout); 711 return true; 712 } 713 714 @Override 715 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 716 float viewInitialY, float dx, float dy) { 717 setFlyoutStateForDragLength(dx); 718 } 719 720 @Override 721 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 722 float viewInitialY, float dx, float dy, float velX, float velY) { 723 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 724 final boolean metRequiredVelocity = 725 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; 726 final boolean metRequiredDeltaX = 727 onLeft 728 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS 729 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; 730 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; 731 final boolean shouldDismiss = metRequiredVelocity 732 || (metRequiredDeltaX && !isCancelFling); 733 734 mFlyout.removeCallbacks(mHideFlyout); 735 animateFlyoutCollapsed(shouldDismiss, velX); 736 737 maybeShowStackUserEducation(); 738 } 739 }; 740 741 private View mDismissTargetCircle; 742 private ViewGroup mDismissTargetContainer; 743 private PhysicsAnimator<View> mDismissTargetAnimator; 744 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig( 745 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 746 747 private int mOrientation = Configuration.ORIENTATION_UNDEFINED; 748 749 @Nullable 750 private BubbleOverflow mBubbleOverflow; 751 752 private boolean mShouldShowUserEducation; 753 private boolean mAnimatingEducationAway; 754 private View mUserEducationView; 755 756 private boolean mShouldShowManageEducation; 757 private BubbleManageEducationView mManageEducationView; 758 private boolean mAnimatingManageEducationAway; 759 760 private ViewGroup mManageMenu; 761 private ImageView mManageSettingsIcon; 762 private TextView mManageSettingsText; 763 private boolean mShowingManage = false; 764 private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( 765 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 766 @SuppressLint("ClickableViewAccessibility") BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, SysUiState sysUiState, Runnable allBubblesAnimatedOutAction, Consumer<Boolean> onImeVisibilityChanged, Runnable hideCurrentInputMethodCallback)767 public BubbleStackView(Context context, BubbleData data, 768 @Nullable SurfaceSynchronizer synchronizer, 769 FloatingContentCoordinator floatingContentCoordinator, 770 SysUiState sysUiState, 771 Runnable allBubblesAnimatedOutAction, 772 Consumer<Boolean> onImeVisibilityChanged, 773 Runnable hideCurrentInputMethodCallback) { 774 super(context); 775 776 mBubbleData = data; 777 mInflater = LayoutInflater.from(context); 778 779 mSysUiState = sysUiState; 780 781 Resources res = getResources(); 782 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); 783 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size); 784 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 785 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 786 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); 787 788 mStatusBarHeight = 789 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); 790 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); 791 792 mDisplaySize = new Point(); 793 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 794 // We use the real size & subtract screen decorations / window insets ourselves when needed 795 wm.getDefaultDisplay().getRealSize(mDisplaySize); 796 797 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 798 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); 799 800 final TypedArray ta = mContext.obtainStyledAttributes( 801 new int[] {android.R.attr.dialogCornerRadius}); 802 mCornerRadius = ta.getDimensionPixelSize(0, 0); 803 ta.recycle(); 804 805 final Runnable onBubbleAnimatedOut = () -> { 806 if (getBubbleCount() == 0) { 807 allBubblesAnimatedOutAction.run(); 808 } 809 }; 810 811 mStackAnimationController = new StackAnimationController( 812 floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut); 813 814 mExpandedAnimationController = new ExpandedAnimationController( 815 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation, 816 onBubbleAnimatedOut); 817 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; 818 819 setUpUserEducation(); 820 821 // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or 822 // is centered. It greatly simplifies translation positioning/animations. Views that will 823 // actually lay out differently in RTL, such as the flyout and expanded view, will set their 824 // layout direction to LOCALE. 825 setLayoutDirection(LAYOUT_DIRECTION_LTR); 826 827 mBubbleContainer = new PhysicsAnimationLayout(context); 828 mBubbleContainer.setActiveController(mStackAnimationController); 829 mBubbleContainer.setElevation(elevation); 830 mBubbleContainer.setClipChildren(false); 831 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 832 833 mExpandedViewContainer = new FrameLayout(context); 834 mExpandedViewContainer.setElevation(elevation); 835 mExpandedViewContainer.setClipChildren(false); 836 addView(mExpandedViewContainer); 837 838 mAnimatingOutSurfaceContainer = new FrameLayout(getContext()); 839 mAnimatingOutSurfaceContainer.setLayoutParams( 840 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 841 addView(mAnimatingOutSurfaceContainer); 842 843 mAnimatingOutSurfaceView = new SurfaceView(getContext()); 844 mAnimatingOutSurfaceView.setUseAlpha(); 845 mAnimatingOutSurfaceView.setZOrderOnTop(true); 846 mAnimatingOutSurfaceView.setCornerRadius(mCornerRadius); 847 mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); 848 mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView); 849 850 mAnimatingOutSurfaceContainer.setPadding( 851 mExpandedViewPadding, 852 mExpandedViewPadding, 853 mExpandedViewPadding, 854 mExpandedViewPadding); 855 856 setUpManageMenu(); 857 858 setUpFlyout(); 859 mFlyoutTransitionSpring.setSpring(new SpringForce() 860 .setStiffness(SpringForce.STIFFNESS_LOW) 861 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 862 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); 863 864 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); 865 mDismissTargetCircle = new DismissCircleView(context); 866 final FrameLayout.LayoutParams newParams = 867 new FrameLayout.LayoutParams(targetSize, targetSize); 868 newParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 869 mDismissTargetCircle.setLayoutParams(newParams); 870 mDismissTargetAnimator = PhysicsAnimator.getInstance(mDismissTargetCircle); 871 872 mDismissTargetContainer = new FrameLayout(context); 873 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams( 874 MATCH_PARENT, 875 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height), 876 Gravity.BOTTOM)); 877 878 final int bottomMargin = 879 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin); 880 mDismissTargetContainer.setPadding(0, 0, 0, bottomMargin); 881 mDismissTargetContainer.setClipToPadding(false); 882 mDismissTargetContainer.setClipChildren(false); 883 mDismissTargetContainer.addView(mDismissTargetCircle); 884 mDismissTargetContainer.setVisibility(View.INVISIBLE); 885 mDismissTargetContainer.setBackgroundResource( 886 R.drawable.floating_dismiss_gradient_transition); 887 addView(mDismissTargetContainer); 888 889 // Start translated down so the target springs up. 890 mDismissTargetCircle.setTranslationY( 891 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height)); 892 893 final ContentResolver contentResolver = getContext().getContentResolver(); 894 final int dismissRadius = Settings.Secure.getInt( 895 contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */); 896 897 // Save the MagneticTarget instance for the newly set up view - we'll add this to the 898 // MagnetizedObjects. 899 mMagneticTarget = new MagnetizedObject.MagneticTarget(mDismissTargetCircle, dismissRadius); 900 901 setClipChildren(false); 902 setFocusable(true); 903 mBubbleContainer.bringToFront(); 904 905 setUpOverflow(); 906 907 mOnImeVisibilityChanged = onImeVisibilityChanged; 908 mHideCurrentInputMethodCallback = hideCurrentInputMethodCallback; 909 910 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { 911 onImeVisibilityChanged.accept(insets.getInsets(WindowInsets.Type.ime()).bottom > 0); 912 913 if (!mIsExpanded || mIsExpansionAnimating) { 914 return view.onApplyWindowInsets(insets); 915 } 916 mExpandedAnimationController.updateYPosition( 917 // Update the insets after we're done translating otherwise position 918 // calculation for them won't be correct. 919 () -> { 920 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 921 mExpandedBubble.getExpandedView().updateInsets(insets); 922 } 923 }); 924 return view.onApplyWindowInsets(insets); 925 }); 926 927 mOrientationChangedListener = 928 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 929 mExpandedAnimationController.updateResources(mOrientation, mDisplaySize); 930 mStackAnimationController.updateResources(mOrientation); 931 mBubbleOverflow.updateDimensions(); 932 933 // Need to update the padding around the view 934 WindowInsets insets = getRootWindowInsets(); 935 int leftPadding = mExpandedViewPadding; 936 int rightPadding = mExpandedViewPadding; 937 if (insets != null) { 938 // Can't have the expanded view overlaying notches 939 int cutoutLeft = 0; 940 int cutoutRight = 0; 941 DisplayCutout cutout = insets.getDisplayCutout(); 942 if (cutout != null) { 943 cutoutLeft = cutout.getSafeInsetLeft(); 944 cutoutRight = cutout.getSafeInsetRight(); 945 } 946 // Or overlaying nav or status bar 947 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft()); 948 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight()); 949 } 950 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding, 951 rightPadding, mExpandedViewPadding); 952 953 if (mIsExpanded) { 954 // Re-draw bubble row and pointer for new orientation. 955 beforeExpandedViewAnimation(); 956 updateOverflowVisibility(); 957 updatePointerPosition(); 958 mExpandedAnimationController.expandFromStack(() -> { 959 afterExpandedViewAnimation(); 960 } /* after */); 961 mExpandedViewContainer.setTranslationX(0); 962 mExpandedViewContainer.setTranslationY(getExpandedViewY()); 963 mExpandedViewContainer.setAlpha(1f); 964 } 965 if (mRelativeStackPositionBeforeRotation != null) { 966 mStackAnimationController.setStackPosition( 967 mRelativeStackPositionBeforeRotation); 968 mRelativeStackPositionBeforeRotation = null; 969 } 970 removeOnLayoutChangeListener(mOrientationChangedListener); 971 }; 972 973 // This must be a separate OnDrawListener since it should be called for every draw. 974 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); 975 976 final ColorMatrix animatedMatrix = new ColorMatrix(); 977 final ColorMatrix darkenMatrix = new ColorMatrix(); 978 979 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f); 980 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> { 981 final float animatedValue = (float) animation.getAnimatedValue(); 982 animatedMatrix.setSaturation(animatedValue); 983 984 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT; 985 darkenMatrix.setScale( 986 1f - animatedDarkenValue /* red */, 987 1f - animatedDarkenValue /* green */, 988 1f - animatedDarkenValue /* blue */, 989 1f /* alpha */); 990 991 // Concat the matrices so that the animatedMatrix both desaturates and darkens. 992 animatedMatrix.postConcat(darkenMatrix); 993 994 // Update the paint and apply it to the bubble container. 995 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix)); 996 997 if (mDesaturateAndDarkenTargetView != null) { 998 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint); 999 } 1000 }); 1001 1002 // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts, 1003 // ActivityViews, etc.) were touched. Collapse the stack if it's expanded. 1004 setOnTouchListener((view, ev) -> { 1005 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 1006 if (mShowingManage) { 1007 showManageMenu(false /* show */); 1008 } else if (mBubbleData.isExpanded()) { 1009 mBubbleData.setExpanded(false); 1010 } 1011 } 1012 1013 return true; 1014 }); 1015 1016 animate() 1017 .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) 1018 .setDuration(CollapsedStatusBarFragment.FADE_IN_DURATION); 1019 } 1020 1021 /** 1022 * Sets whether or not the stack should become temporarily invisible by moving off the side of 1023 * the screen. 1024 * 1025 * If a flyout comes in while it's invisible, it will animate back in while the flyout is 1026 * showing but disappear again when the flyout is gone. 1027 */ setTemporarilyInvisible(boolean invisible)1028 public void setTemporarilyInvisible(boolean invisible) { 1029 mTemporarilyInvisible = invisible; 1030 1031 // If we are animating out, hide immediately if possible so we animate out with the status 1032 // bar. 1033 updateTemporarilyInvisibleAnimation(invisible /* hideImmediately */); 1034 } 1035 1036 /** 1037 * Animates the stack to be temporarily invisible, if needed. 1038 * 1039 * If we're currently dragging the stack, or a flyout is visible, the stack will remain visible. 1040 * regardless of the value of {@link #mTemporarilyInvisible}. This method is called on ACTION_UP 1041 * as well as whenever a flyout hides, so we will animate invisible at that point if needed. 1042 */ updateTemporarilyInvisibleAnimation(boolean hideImmediately)1043 private void updateTemporarilyInvisibleAnimation(boolean hideImmediately) { 1044 removeCallbacks(mAnimateTemporarilyInvisibleImmediate); 1045 1046 if (mIsDraggingStack) { 1047 // If we're dragging the stack, don't animate it invisible. 1048 return; 1049 } 1050 1051 final boolean shouldHide = 1052 mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE; 1053 1054 postDelayed(mAnimateTemporarilyInvisibleImmediate, 1055 shouldHide && !hideImmediately ? ANIMATE_TEMPORARILY_INVISIBLE_DELAY : 0); 1056 } 1057 1058 private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> { 1059 if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) { 1060 if (mStackAnimationController.isStackOnLeftSide()) { 1061 animate().translationX(-mBubbleSize).start(); 1062 } else { 1063 animate().translationX(mBubbleSize).start(); 1064 } 1065 } else { 1066 animate().translationX(0).start(); 1067 } 1068 }; 1069 setUpManageMenu()1070 private void setUpManageMenu() { 1071 if (mManageMenu != null) { 1072 removeView(mManageMenu); 1073 } 1074 1075 mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( 1076 R.layout.bubble_manage_menu, this, false); 1077 mManageMenu.setVisibility(View.INVISIBLE); 1078 1079 PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); 1080 1081 mManageMenu.setOutlineProvider(new ViewOutlineProvider() { 1082 @Override 1083 public void getOutline(View view, Outline outline) { 1084 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 1085 } 1086 }); 1087 mManageMenu.setClipToOutline(true); 1088 1089 mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( 1090 view -> { 1091 showManageMenu(false /* show */); 1092 dismissBubbleIfExists(mBubbleData.getSelectedBubble()); 1093 }); 1094 1095 mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( 1096 view -> { 1097 showManageMenu(false /* show */); 1098 mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey()); 1099 }); 1100 1101 mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( 1102 view -> { 1103 showManageMenu(false /* show */); 1104 final Bubble bubble = mBubbleData.getSelectedBubble(); 1105 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 1106 final Intent intent = bubble.getSettingsIntent(mContext); 1107 collapseStack(() -> { 1108 mContext.startActivityAsUser(intent, bubble.getUser()); 1109 logBubbleEvent(bubble, 1110 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); 1111 }); 1112 } 1113 }); 1114 1115 mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); 1116 mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); 1117 1118 // The menu itself should respect locale direction so the icons are on the correct side. 1119 mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 1120 addView(mManageMenu); 1121 } 1122 setUpUserEducation()1123 private void setUpUserEducation() { 1124 if (mUserEducationView != null) { 1125 removeView(mUserEducationView); 1126 } 1127 mShouldShowUserEducation = shouldShowBubblesEducation(); 1128 if (DEBUG_USER_EDUCATION) { 1129 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation); 1130 } 1131 if (mShouldShowUserEducation) { 1132 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this, 1133 false /* attachToRoot */); 1134 mUserEducationView.setVisibility(GONE); 1135 1136 final TypedArray ta = mContext.obtainStyledAttributes( 1137 new int[] {android.R.attr.colorAccent, 1138 android.R.attr.textColorPrimaryInverse}); 1139 final int bgColor = ta.getColor(0, Color.BLACK); 1140 int textColor = ta.getColor(1, Color.WHITE); 1141 ta.recycle(); 1142 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true); 1143 1144 TextView title = mUserEducationView.findViewById(R.id.user_education_title); 1145 TextView description = mUserEducationView.findViewById(R.id.user_education_description); 1146 title.setTextColor(textColor); 1147 description.setTextColor(textColor); 1148 1149 updateUserEducationForLayoutDirection(); 1150 addView(mUserEducationView); 1151 } 1152 1153 if (mManageEducationView != null) { 1154 removeView(mManageEducationView); 1155 } 1156 mShouldShowManageEducation = shouldShowManageEducation(); 1157 if (DEBUG_USER_EDUCATION) { 1158 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation); 1159 } 1160 if (mShouldShowManageEducation) { 1161 mManageEducationView = (BubbleManageEducationView) 1162 mInflater.inflate(R.layout.bubbles_manage_button_education, this, 1163 false /* attachToRoot */); 1164 mManageEducationView.setVisibility(GONE); 1165 mManageEducationView.setElevation(mBubbleElevation); 1166 mManageEducationView.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); 1167 addView(mManageEducationView); 1168 } 1169 } 1170 1171 @SuppressLint("ClickableViewAccessibility") setUpFlyout()1172 private void setUpFlyout() { 1173 if (mFlyout != null) { 1174 removeView(mFlyout); 1175 } 1176 mFlyout = new BubbleFlyoutView(getContext()); 1177 mFlyout.setVisibility(GONE); 1178 mFlyout.animate() 1179 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION) 1180 .setInterpolator(new AccelerateDecelerateInterpolator()); 1181 mFlyout.setOnClickListener(mFlyoutClickListener); 1182 mFlyout.setOnTouchListener(mFlyoutTouchListener); 1183 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 1184 } 1185 setUpOverflow()1186 private void setUpOverflow() { 1187 int overflowBtnIndex = 0; 1188 if (mBubbleOverflow == null) { 1189 mBubbleOverflow = new BubbleOverflow(getContext()); 1190 mBubbleOverflow.setUpOverflow(mBubbleContainer, this); 1191 } else { 1192 mBubbleContainer.removeView(mBubbleOverflow.getIconView()); 1193 mBubbleOverflow.setUpOverflow(mBubbleContainer, this); 1194 overflowBtnIndex = mBubbleContainer.getChildCount(); 1195 } 1196 mBubbleContainer.addView(mBubbleOverflow.getIconView(), overflowBtnIndex, 1197 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 1198 mBubbleOverflow.getIconView().setOnClickListener(v -> { 1199 setSelectedBubble(mBubbleOverflow); 1200 showManageMenu(false); 1201 }); 1202 updateOverflowVisibility(); 1203 } 1204 /** 1205 * Handle theme changes. 1206 */ onThemeChanged()1207 public void onThemeChanged() { 1208 setUpFlyout(); 1209 setUpOverflow(); 1210 setUpUserEducation(); 1211 setUpManageMenu(); 1212 updateExpandedViewTheme(); 1213 } 1214 1215 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ onOrientationChanged(int orientation)1216 public void onOrientationChanged(int orientation) { 1217 mOrientation = orientation; 1218 1219 // Display size is based on the rotation device was in when requested, we should update it 1220 // We use the real size & subtract screen decorations / window insets ourselves when needed 1221 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); 1222 wm.getDefaultDisplay().getRealSize(mDisplaySize); 1223 1224 // Some resources change depending on orientation 1225 Resources res = getContext().getResources(); 1226 mStatusBarHeight = res.getDimensionPixelSize( 1227 com.android.internal.R.dimen.status_bar_height); 1228 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 1229 1230 mRelativeStackPositionBeforeRotation = mStackAnimationController.getRelativeStackPosition(); 1231 addOnLayoutChangeListener(mOrientationChangedListener); 1232 hideFlyoutImmediate(); 1233 1234 mManageMenu.setVisibility(View.INVISIBLE); 1235 mShowingManage = false; 1236 } 1237 1238 /** Tells the views with locale-dependent layout direction to resolve the new direction. */ onLayoutDirectionChanged(int direction)1239 public void onLayoutDirectionChanged(int direction) { 1240 mManageMenu.setLayoutDirection(direction); 1241 mFlyout.setLayoutDirection(direction); 1242 if (mUserEducationView != null) { 1243 mUserEducationView.setLayoutDirection(direction); 1244 updateUserEducationForLayoutDirection(); 1245 } 1246 if (mManageEducationView != null) { 1247 mManageEducationView.setLayoutDirection(direction); 1248 } 1249 updateExpandedViewDirection(direction); 1250 } 1251 1252 /** Respond to the display size change by recalculating view size and location. */ onDisplaySizeChanged()1253 public void onDisplaySizeChanged() { 1254 setUpOverflow(); 1255 1256 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); 1257 wm.getDefaultDisplay().getRealSize(mDisplaySize); 1258 Resources res = getContext().getResources(); 1259 mStatusBarHeight = res.getDimensionPixelSize( 1260 com.android.internal.R.dimen.status_bar_height); 1261 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); 1262 mBubbleSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size); 1263 for (Bubble b : mBubbleData.getBubbles()) { 1264 if (b.getIconView() == null) { 1265 Log.d(TAG, "Display size changed. Icon null: " + b); 1266 continue; 1267 } 1268 b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); 1269 } 1270 mExpandedAnimationController.updateResources(mOrientation, mDisplaySize); 1271 mStackAnimationController.updateResources(mOrientation); 1272 1273 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); 1274 mDismissTargetCircle.getLayoutParams().width = targetSize; 1275 mDismissTargetCircle.getLayoutParams().height = targetSize; 1276 mDismissTargetCircle.requestLayout(); 1277 1278 mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); 1279 } 1280 1281 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)1282 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 1283 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 1284 1285 mTempRect.setEmpty(); 1286 getTouchableRegion(mTempRect); 1287 inoutInfo.touchableRegion.set(mTempRect); 1288 } 1289 1290 @Override onAttachedToWindow()1291 protected void onAttachedToWindow() { 1292 super.onAttachedToWindow(); 1293 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 1294 } 1295 1296 @Override onDetachedFromWindow()1297 protected void onDetachedFromWindow() { 1298 super.onDetachedFromWindow(); 1299 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 1300 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 1301 if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) { 1302 mBubbleOverflow.getExpandedView().cleanUpExpandedState(); 1303 } 1304 } 1305 1306 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1307 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1308 super.onInitializeAccessibilityNodeInfoInternal(info); 1309 setupLocalMenu(info); 1310 } 1311 updateExpandedViewTheme()1312 void updateExpandedViewTheme() { 1313 final List<Bubble> bubbles = mBubbleData.getBubbles(); 1314 if (bubbles.isEmpty()) { 1315 return; 1316 } 1317 bubbles.forEach(bubble -> { 1318 if (bubble.getExpandedView() != null) { 1319 bubble.getExpandedView().applyThemeAttrs(); 1320 } 1321 }); 1322 } 1323 updateExpandedViewDirection(int direction)1324 void updateExpandedViewDirection(int direction) { 1325 final List<Bubble> bubbles = mBubbleData.getBubbles(); 1326 if (bubbles.isEmpty()) { 1327 return; 1328 } 1329 bubbles.forEach(bubble -> { 1330 if (bubble.getExpandedView() != null) { 1331 bubble.getExpandedView().setLayoutDirection(direction); 1332 } 1333 }); 1334 } 1335 setupLocalMenu(AccessibilityNodeInfo info)1336 void setupLocalMenu(AccessibilityNodeInfo info) { 1337 Resources res = mContext.getResources(); 1338 1339 // Custom local actions. 1340 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left, 1341 res.getString(R.string.bubble_accessibility_action_move_top_left)); 1342 info.addAction(moveTopLeft); 1343 1344 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right, 1345 res.getString(R.string.bubble_accessibility_action_move_top_right)); 1346 info.addAction(moveTopRight); 1347 1348 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left, 1349 res.getString(R.string.bubble_accessibility_action_move_bottom_left)); 1350 info.addAction(moveBottomLeft); 1351 1352 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right, 1353 res.getString(R.string.bubble_accessibility_action_move_bottom_right)); 1354 info.addAction(moveBottomRight); 1355 1356 // Default actions. 1357 info.addAction(AccessibilityAction.ACTION_DISMISS); 1358 if (mIsExpanded) { 1359 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 1360 } else { 1361 info.addAction(AccessibilityAction.ACTION_EXPAND); 1362 } 1363 } 1364 1365 @Override performAccessibilityActionInternal(int action, Bundle arguments)1366 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1367 if (super.performAccessibilityActionInternal(action, arguments)) { 1368 return true; 1369 } 1370 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion(); 1371 1372 // R constants are not final so we cannot use switch-case here. 1373 if (action == AccessibilityNodeInfo.ACTION_DISMISS) { 1374 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION); 1375 announceForAccessibility( 1376 getResources().getString(R.string.accessibility_bubble_dismissed)); 1377 return true; 1378 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { 1379 mBubbleData.setExpanded(false); 1380 return true; 1381 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) { 1382 mBubbleData.setExpanded(true); 1383 return true; 1384 } else if (action == R.id.action_move_top_left) { 1385 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top); 1386 return true; 1387 } else if (action == R.id.action_move_top_right) { 1388 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top); 1389 return true; 1390 } else if (action == R.id.action_move_bottom_left) { 1391 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom); 1392 return true; 1393 } else if (action == R.id.action_move_bottom_right) { 1394 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom); 1395 return true; 1396 } 1397 return false; 1398 } 1399 1400 /** 1401 * Update content description for a11y TalkBack. 1402 */ updateContentDescription()1403 public void updateContentDescription() { 1404 if (mBubbleData.getBubbles().isEmpty()) { 1405 return; 1406 } 1407 1408 for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { 1409 final Bubble bubble = mBubbleData.getBubbles().get(i); 1410 final String appName = bubble.getAppName(); 1411 1412 String titleStr = bubble.getTitle(); 1413 if (titleStr == null) { 1414 titleStr = getResources().getString(R.string.notification_bubble_title); 1415 } 1416 1417 if (bubble.getIconView() != null) { 1418 if (mIsExpanded || i > 0) { 1419 bubble.getIconView().setContentDescription(getResources().getString( 1420 R.string.bubble_content_description_single, titleStr, appName)); 1421 } else { 1422 final int moreCount = mBubbleContainer.getChildCount() - 1; 1423 bubble.getIconView().setContentDescription(getResources().getString( 1424 R.string.bubble_content_description_stack, 1425 titleStr, appName, moreCount)); 1426 } 1427 } 1428 } 1429 } 1430 updateSystemGestureExcludeRects()1431 private void updateSystemGestureExcludeRects() { 1432 // Exclude the region occupied by the first BubbleView in the stack 1433 Rect excludeZone = mSystemGestureExclusionRects.get(0); 1434 if (getBubbleCount() > 0) { 1435 View firstBubble = mBubbleContainer.getChildAt(0); 1436 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(), 1437 firstBubble.getBottom()); 1438 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f), 1439 (int) (firstBubble.getTranslationY() + 0.5f)); 1440 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects); 1441 } else { 1442 excludeZone.setEmpty(); 1443 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList()); 1444 } 1445 } 1446 1447 /** 1448 * Sets the listener to notify when the bubble stack is expanded. 1449 */ setExpandListener(BubbleController.BubbleExpandListener listener)1450 public void setExpandListener(BubbleController.BubbleExpandListener listener) { 1451 mExpandListener = listener; 1452 } 1453 1454 /** Sets the function to call to un-bubble the given conversation. */ setUnbubbleConversationCallback( Consumer<String> unbubbleConversationCallback)1455 public void setUnbubbleConversationCallback( 1456 Consumer<String> unbubbleConversationCallback) { 1457 mUnbubbleConversationCallback = unbubbleConversationCallback; 1458 } 1459 1460 /** 1461 * Whether the stack of bubbles is expanded or not. 1462 */ isExpanded()1463 public boolean isExpanded() { 1464 return mIsExpanded; 1465 } 1466 1467 /** 1468 * Whether the stack of bubbles is animating to or from expansion. 1469 */ isExpansionAnimating()1470 public boolean isExpansionAnimating() { 1471 return mIsExpansionAnimating; 1472 } 1473 1474 /** 1475 * The {@link BadgedImageView} that is expanded, null if one does not exist. 1476 */ getExpandedBubbleView()1477 View getExpandedBubbleView() { 1478 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null; 1479 } 1480 1481 /** 1482 * The {@link Bubble} that is expanded, null if one does not exist. 1483 */ 1484 @Nullable getExpandedBubble()1485 BubbleViewProvider getExpandedBubble() { 1486 return mExpandedBubble; 1487 } 1488 1489 // via BubbleData.Listener 1490 @SuppressLint("ClickableViewAccessibility") addBubble(Bubble bubble)1491 void addBubble(Bubble bubble) { 1492 if (DEBUG_BUBBLE_STACK_VIEW) { 1493 Log.d(TAG, "addBubble: " + bubble); 1494 } 1495 1496 if (getBubbleCount() == 0 && mShouldShowUserEducation) { 1497 // Override the default stack position if we're showing user education. 1498 mStackAnimationController.setStackPosition( 1499 mStackAnimationController.getStartPosition()); 1500 } 1501 1502 if (getBubbleCount() == 0) { 1503 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 1504 } 1505 1506 if (bubble.getIconView() == null) { 1507 return; 1508 } 1509 1510 // Set the dot position to the opposite of the side the stack is resting on, since the stack 1511 // resting slightly off-screen would result in the dot also being off-screen. 1512 bubble.getIconView().setDotPositionOnLeft( 1513 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */); 1514 1515 bubble.getIconView().setOnClickListener(mBubbleClickListener); 1516 bubble.getIconView().setOnTouchListener(mBubbleTouchListener); 1517 1518 mBubbleContainer.addView(bubble.getIconView(), 0, 1519 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 1520 animateInFlyoutForBubble(bubble); 1521 requestUpdate(); 1522 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); 1523 } 1524 1525 // via BubbleData.Listener removeBubble(Bubble bubble)1526 void removeBubble(Bubble bubble) { 1527 if (DEBUG_BUBBLE_STACK_VIEW) { 1528 Log.d(TAG, "removeBubble: " + bubble); 1529 } 1530 // Remove it from the views 1531 for (int i = 0; i < getBubbleCount(); i++) { 1532 View v = mBubbleContainer.getChildAt(i); 1533 if (v instanceof BadgedImageView 1534 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) { 1535 mBubbleContainer.removeViewAt(i); 1536 bubble.cleanupViews(); 1537 updatePointerPosition(); 1538 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); 1539 return; 1540 } 1541 } 1542 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); 1543 } 1544 updateOverflowVisibility()1545 private void updateOverflowVisibility() { 1546 if (mBubbleOverflow == null) { 1547 return; 1548 } 1549 mBubbleOverflow.setVisible(mIsExpanded ? VISIBLE : GONE); 1550 } 1551 1552 // via BubbleData.Listener updateBubble(Bubble bubble)1553 void updateBubble(Bubble bubble) { 1554 animateInFlyoutForBubble(bubble); 1555 requestUpdate(); 1556 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); 1557 } 1558 updateBubbleOrder(List<Bubble> bubbles)1559 public void updateBubbleOrder(List<Bubble> bubbles) { 1560 for (int i = 0; i < bubbles.size(); i++) { 1561 Bubble bubble = bubbles.get(i); 1562 mBubbleContainer.reorderView(bubble.getIconView(), i); 1563 } 1564 updateBubbleZOrdersAndDotPosition(false /* animate */); 1565 updatePointerPosition(); 1566 } 1567 1568 /** 1569 * Changes the currently selected bubble. If the stack is already expanded, the newly selected 1570 * bubble will be shown immediately. This does not change the expanded state or change the 1571 * position of any bubble. 1572 */ 1573 // via BubbleData.Listener setSelectedBubble(@ullable BubbleViewProvider bubbleToSelect)1574 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) { 1575 if (DEBUG_BUBBLE_STACK_VIEW) { 1576 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); 1577 } 1578 1579 if (bubbleToSelect == null) { 1580 mBubbleData.setShowingOverflow(false); 1581 return; 1582 } 1583 1584 // Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want 1585 // to re-render it even if it has the same key (equals() returns true). If the currently 1586 // expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance 1587 // with the same key (with newly inflated expanded views), and we need to render those new 1588 // views. 1589 if (mExpandedBubble == bubbleToSelect) { 1590 return; 1591 } 1592 1593 if (bubbleToSelect.getKey() == BubbleOverflow.KEY) { 1594 mBubbleData.setShowingOverflow(true); 1595 } else { 1596 mBubbleData.setShowingOverflow(false); 1597 } 1598 1599 if (mIsExpanded && mIsExpansionAnimating) { 1600 // If the bubble selection changed during the expansion animation, the expanding bubble 1601 // probably crashed or immediately removed itself (or, we just got unlucky with a new 1602 // auto-expanding bubble showing up at just the right time). Cancel the animations so we 1603 // can start fresh. 1604 cancelAllExpandCollapseSwitchAnimations(); 1605 } 1606 1607 // If we're expanded, screenshot the currently expanded bubble (before expanding the newly 1608 // selected bubble) so we can animate it out. 1609 if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 1610 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 1611 // Before screenshotting, have the real ActivityView show on top of other surfaces 1612 // so that the screenshot doesn't flicker on top of it. 1613 mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true); 1614 } 1615 1616 try { 1617 screenshotAnimatingOutBubbleIntoSurface((success) -> { 1618 mAnimatingOutSurfaceContainer.setVisibility( 1619 success ? View.VISIBLE : View.INVISIBLE); 1620 showNewlySelectedBubble(bubbleToSelect); 1621 }); 1622 } catch (Exception e) { 1623 showNewlySelectedBubble(bubbleToSelect); 1624 e.printStackTrace(); 1625 } 1626 } else { 1627 showNewlySelectedBubble(bubbleToSelect); 1628 } 1629 } 1630 showNewlySelectedBubble(BubbleViewProvider bubbleToSelect)1631 private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { 1632 final BubbleViewProvider previouslySelected = mExpandedBubble; 1633 mExpandedBubble = bubbleToSelect; 1634 updatePointerPosition(); 1635 1636 if (mIsExpanded) { 1637 hideCurrentInputMethod(); 1638 1639 // Make the container of the expanded view transparent before removing the expanded view 1640 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the 1641 // expanded view becomes visible on the screen. See b/126856255 1642 mExpandedViewContainer.setAlpha(0.0f); 1643 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 1644 if (previouslySelected != null) { 1645 previouslySelected.setContentVisibility(false); 1646 } 1647 1648 updateExpandedBubble(); 1649 requestUpdate(); 1650 1651 logBubbleEvent(previouslySelected, 1652 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 1653 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 1654 notifyExpansionChanged(previouslySelected, false /* expanded */); 1655 notifyExpansionChanged(bubbleToSelect, true /* expanded */); 1656 }); 1657 } 1658 } 1659 1660 /** 1661 * Changes the expanded state of the stack. 1662 * 1663 * @param shouldExpand whether the bubble stack should appear expanded 1664 */ 1665 // via BubbleData.Listener setExpanded(boolean shouldExpand)1666 public void setExpanded(boolean shouldExpand) { 1667 if (DEBUG_BUBBLE_STACK_VIEW) { 1668 Log.d(TAG, "setExpanded: " + shouldExpand); 1669 } 1670 1671 if (!shouldExpand) { 1672 // If we're collapsing, release the animating-out surface immediately since we have no 1673 // need for it, and this ensures it cannot remain visible as we collapse. 1674 releaseAnimatingOutBubbleBuffer(); 1675 } 1676 1677 if (shouldExpand == mIsExpanded) { 1678 return; 1679 } 1680 1681 hideCurrentInputMethod(); 1682 1683 mSysUiState 1684 .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) 1685 .commitUpdate(mContext.getDisplayId()); 1686 1687 if (mIsExpanded) { 1688 animateCollapse(); 1689 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 1690 } else { 1691 animateExpansion(); 1692 // TODO: move next line to BubbleData 1693 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 1694 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); 1695 } 1696 notifyExpansionChanged(mExpandedBubble, mIsExpanded); 1697 } 1698 1699 /** 1700 * If necessary, shows the user education view for the bubble stack. This appears the first 1701 * time a user taps on a bubble. 1702 * 1703 * @return true if user education was shown, false otherwise. 1704 */ maybeShowStackUserEducation()1705 private boolean maybeShowStackUserEducation() { 1706 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) { 1707 mUserEducationView.setAlpha(0); 1708 mUserEducationView.setVisibility(VISIBLE); 1709 updateUserEducationForLayoutDirection(); 1710 1711 // Post so we have height of mUserEducationView 1712 mUserEducationView.post(() -> { 1713 final int viewHeight = mUserEducationView.getHeight(); 1714 PointF stackPosition = mStackAnimationController.getStartPosition(); 1715 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2); 1716 mUserEducationView.setTranslationY(translationY); 1717 mUserEducationView.animate() 1718 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) 1719 .setInterpolator(FAST_OUT_SLOW_IN) 1720 .alpha(1); 1721 }); 1722 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true); 1723 return true; 1724 } 1725 return false; 1726 } 1727 updateUserEducationForLayoutDirection()1728 private void updateUserEducationForLayoutDirection() { 1729 if (mUserEducationView == null) { 1730 return; 1731 } 1732 LinearLayout textLayout = mUserEducationView.findViewById(R.id.user_education_view); 1733 TextView title = mUserEducationView.findViewById(R.id.user_education_title); 1734 TextView description = mUserEducationView.findViewById(R.id.user_education_description); 1735 boolean isLtr = 1736 getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR; 1737 if (isLtr) { 1738 mUserEducationView.setLayoutDirection(LAYOUT_DIRECTION_LTR); 1739 textLayout.setBackgroundResource(R.drawable.bubble_stack_user_education_bg); 1740 title.setGravity(Gravity.LEFT); 1741 description.setGravity(Gravity.LEFT); 1742 } else { 1743 mUserEducationView.setLayoutDirection(LAYOUT_DIRECTION_RTL); 1744 textLayout.setBackgroundResource(R.drawable.bubble_stack_user_education_bg_rtl); 1745 title.setGravity(Gravity.RIGHT); 1746 description.setGravity(Gravity.RIGHT); 1747 } 1748 } 1749 1750 /** 1751 * If necessary, hides the user education view for the bubble stack. 1752 * 1753 * @param fromExpansion if true this indicates the hide is happening due to the bubble being 1754 * expanded, false if due to a touch outside of the bubble stack. 1755 */ hideStackUserEducation(boolean fromExpansion)1756 void hideStackUserEducation(boolean fromExpansion) { 1757 if (mShouldShowUserEducation 1758 && mUserEducationView.getVisibility() == VISIBLE 1759 && !mAnimatingEducationAway) { 1760 mAnimatingEducationAway = true; 1761 mUserEducationView.animate() 1762 .alpha(0) 1763 .setDuration(fromExpansion 1764 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT 1765 : ANIMATE_STACK_USER_EDUCATION_DURATION) 1766 .withEndAction(() -> { 1767 mAnimatingEducationAway = false; 1768 mShouldShowUserEducation = shouldShowBubblesEducation(); 1769 mUserEducationView.setVisibility(GONE); 1770 }); 1771 } 1772 } 1773 1774 /** 1775 * If necessary, toggles the user education view for the manage button. This is shown when the 1776 * bubble stack is expanded for the first time. 1777 * 1778 * @param show whether the user education view should show or not. 1779 */ maybeShowManageEducation(boolean show)1780 void maybeShowManageEducation(boolean show) { 1781 if (mManageEducationView == null) { 1782 return; 1783 } 1784 if (show 1785 && mShouldShowManageEducation 1786 && mManageEducationView.getVisibility() != VISIBLE 1787 && mIsExpanded 1788 && mExpandedBubble.getExpandedView() != null) { 1789 mManageEducationView.setAlpha(0); 1790 mManageEducationView.setVisibility(VISIBLE); 1791 mManageEducationView.post(() -> { 1792 mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); 1793 final int viewHeight = mManageEducationView.getManageViewHeight(); 1794 final int inset = getResources().getDimensionPixelSize( 1795 R.dimen.bubbles_manage_education_top_inset); 1796 mManageEducationView.bringToFront(); 1797 mManageEducationView.setManageViewPosition(0, mTempRect.top - viewHeight + inset); 1798 mManageEducationView.animate() 1799 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) 1800 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1); 1801 mManageEducationView.findViewById(R.id.manage).setOnClickListener(view -> { 1802 mExpandedBubble.getExpandedView().findViewById(R.id.settings_button) 1803 .performClick(); 1804 maybeShowManageEducation(false); 1805 }); 1806 mManageEducationView.findViewById(R.id.got_it).setOnClickListener(view -> 1807 maybeShowManageEducation(false)); 1808 mManageEducationView.setOnClickListener(view -> 1809 maybeShowManageEducation(false)); 1810 }); 1811 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true); 1812 } else if (!show 1813 && mManageEducationView.getVisibility() == VISIBLE 1814 && !mAnimatingManageEducationAway) { 1815 mManageEducationView.animate() 1816 .alpha(0) 1817 .setDuration(mIsExpansionAnimating 1818 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT 1819 : ANIMATE_STACK_USER_EDUCATION_DURATION) 1820 .withEndAction(() -> { 1821 mAnimatingManageEducationAway = false; 1822 mShouldShowManageEducation = shouldShowManageEducation(); 1823 mManageEducationView.setVisibility(GONE); 1824 }); 1825 } 1826 } 1827 1828 /** 1829 * Dismiss the stack of bubbles. 1830 * 1831 * @deprecated 1832 */ 1833 @Deprecated stackDismissed(int reason)1834 void stackDismissed(int reason) { 1835 if (DEBUG_BUBBLE_STACK_VIEW) { 1836 Log.d(TAG, "stackDismissed: reason=" + reason); 1837 } 1838 mBubbleData.dismissAll(reason); 1839 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */, 1840 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED); 1841 } 1842 1843 /** 1844 * @deprecated use {@link #setExpanded(boolean)} and 1845 * {@link BubbleData#setSelectedBubble(Bubble)} 1846 */ 1847 @Deprecated 1848 @MainThread collapseStack(Runnable endRunnable)1849 void collapseStack(Runnable endRunnable) { 1850 if (DEBUG_BUBBLE_STACK_VIEW) { 1851 Log.d(TAG, "collapseStack(endRunnable)"); 1852 } 1853 mBubbleData.setExpanded(false); 1854 // TODO - use the runnable at end of animation 1855 endRunnable.run(); 1856 } 1857 showExpandedViewContents(int displayId)1858 void showExpandedViewContents(int displayId) { 1859 if (mExpandedBubble != null 1860 && mExpandedBubble.getExpandedView() != null 1861 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) { 1862 mExpandedBubble.setContentVisibility(true); 1863 } 1864 } 1865 1866 /** 1867 * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or 1868 * not. 1869 */ hideCurrentInputMethod()1870 void hideCurrentInputMethod() { 1871 mHideCurrentInputMethodCallback.run(); 1872 } 1873 beforeExpandedViewAnimation()1874 private void beforeExpandedViewAnimation() { 1875 mIsExpansionAnimating = true; 1876 hideFlyoutImmediate(); 1877 updateExpandedBubble(); 1878 updateExpandedView(); 1879 } 1880 afterExpandedViewAnimation()1881 private void afterExpandedViewAnimation() { 1882 mIsExpansionAnimating = false; 1883 updateExpandedView(); 1884 requestUpdate(); 1885 } 1886 animateExpansion()1887 private void animateExpansion() { 1888 cancelDelayedExpandCollapseSwitchAnimations(); 1889 1890 mIsExpanded = true; 1891 hideStackUserEducation(true /* fromExpansion */); 1892 beforeExpandedViewAnimation(); 1893 1894 mBubbleContainer.setActiveController(mExpandedAnimationController); 1895 updateOverflowVisibility(); 1896 updatePointerPosition(); 1897 mExpandedAnimationController.expandFromStack(() -> { 1898 afterExpandedViewAnimation(); 1899 maybeShowManageEducation(true); 1900 } /* after */); 1901 1902 mExpandedViewContainer.setTranslationX(0); 1903 mExpandedViewContainer.setTranslationY(getExpandedViewY()); 1904 mExpandedViewContainer.setAlpha(1f); 1905 1906 // X-value of the bubble we're expanding, once it's settled in its row. 1907 final float bubbleWillBeAtX = 1908 mExpandedAnimationController.getBubbleLeft( 1909 mBubbleData.getBubbles().indexOf(mExpandedBubble)); 1910 1911 // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles 1912 // that are animating farther, so that the expanded view doesn't move as much. 1913 final float horizontalDistanceAnimated = 1914 Math.abs(bubbleWillBeAtX 1915 - mStackAnimationController.getStackPosition().x); 1916 1917 // Wait for the path animation target to reach its end, and add a small amount of extra time 1918 // if the bubble is moving a lot horizontally. 1919 long startDelay = 0L; 1920 1921 // Should not happen since we lay out before expanding, but just in case... 1922 if (getWidth() > 0) { 1923 startDelay = (long) 1924 (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION 1925 + (horizontalDistanceAnimated / getWidth()) * 30); 1926 } 1927 1928 // Set the pivot point for the scale, so the expanded view animates out from the bubble. 1929 mExpandedViewContainerMatrix.setScale( 1930 0f, 0f, 1931 bubbleWillBeAtX + mBubbleSize / 2f, getExpandedViewY()); 1932 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 1933 1934 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 1935 mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); 1936 } 1937 1938 mDelayedAnimationHandler.postDelayed(() -> { 1939 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 1940 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 1941 .spring(AnimatableScaleMatrix.SCALE_X, 1942 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 1943 mScaleInSpringConfig) 1944 .spring(AnimatableScaleMatrix.SCALE_Y, 1945 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 1946 mScaleInSpringConfig) 1947 .addUpdateListener((target, values) -> { 1948 if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) { 1949 return; 1950 } 1951 mExpandedViewContainerMatrix.postTranslate( 1952 mExpandedBubble.getIconView().getTranslationX() 1953 - bubbleWillBeAtX, 1954 0); 1955 mExpandedViewContainer.setAnimationMatrix( 1956 mExpandedViewContainerMatrix); 1957 }) 1958 .withEndActions(() -> { 1959 if (mExpandedBubble != null 1960 && mExpandedBubble.getExpandedView() != null) { 1961 mExpandedBubble.getExpandedView() 1962 .setContentVisibility(true); 1963 mExpandedBubble.getExpandedView() 1964 .setSurfaceZOrderedOnTop(false); 1965 } 1966 }) 1967 .start(); 1968 }, startDelay); 1969 } 1970 animateCollapse()1971 private void animateCollapse() { 1972 cancelDelayedExpandCollapseSwitchAnimations(); 1973 1974 // Hide the menu if it's visible. 1975 showManageMenu(false); 1976 1977 mIsExpanded = false; 1978 mIsExpansionAnimating = true; 1979 1980 mBubbleContainer.cancelAllAnimations(); 1981 1982 // If we were in the middle of swapping, the animating-out surface would have been scaling 1983 // to zero - finish it off. 1984 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 1985 mAnimatingOutSurfaceContainer.setScaleX(0f); 1986 mAnimatingOutSurfaceContainer.setScaleY(0f); 1987 1988 // Let the expanded animation controller know that it shouldn't animate child adds/reorders 1989 // since we're about to animate collapsed. 1990 mExpandedAnimationController.notifyPreparingToCollapse(); 1991 1992 final long startDelay = 1993 (long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 0.6f); 1994 mDelayedAnimationHandler.postDelayed(() -> mExpandedAnimationController.collapseBackToStack( 1995 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge() 1996 /* collapseTo */, 1997 () -> mBubbleContainer.setActiveController(mStackAnimationController)), startDelay); 1998 1999 // We want to visually collapse into this bubble during the animation. 2000 final View expandingFromBubble = mExpandedBubble.getIconView(); 2001 2002 // X-value the bubble is animating from (back into the stack). 2003 final float expandingFromBubbleAtX = 2004 mExpandedAnimationController.getBubbleLeft( 2005 mBubbleData.getBubbles().indexOf(mExpandedBubble)); 2006 2007 // Set the pivot point. 2008 mExpandedViewContainerMatrix.setScale( 2009 1f, 1f, 2010 expandingFromBubbleAtX + mBubbleSize / 2f, 2011 getExpandedViewY()); 2012 2013 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2014 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2015 .spring(AnimatableScaleMatrix.SCALE_X, 0f, mScaleOutSpringConfig) 2016 .spring(AnimatableScaleMatrix.SCALE_Y, 0f, mScaleOutSpringConfig) 2017 .addUpdateListener((target, values) -> { 2018 if (expandingFromBubble != null) { 2019 // Follow the bubble as it translates! 2020 mExpandedViewContainerMatrix.postTranslate( 2021 expandingFromBubble.getTranslationX() 2022 - expandingFromBubbleAtX, 0f); 2023 } 2024 2025 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2026 2027 // Hide early so we don't have a tiny little expanded view still visible at the 2028 // end of the scale animation. 2029 if (mExpandedViewContainerMatrix.getScaleX() < 0.05f) { 2030 mExpandedViewContainer.setVisibility(View.INVISIBLE); 2031 } 2032 }) 2033 .withEndActions(() -> { 2034 final BubbleViewProvider previouslySelected = mExpandedBubble; 2035 beforeExpandedViewAnimation(); 2036 maybeShowManageEducation(false); 2037 2038 if (DEBUG_BUBBLE_STACK_VIEW) { 2039 Log.d(TAG, "animateCollapse"); 2040 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(), 2041 mExpandedBubble)); 2042 } 2043 updateOverflowVisibility(); 2044 2045 afterExpandedViewAnimation(); 2046 if (previouslySelected != null) { 2047 previouslySelected.setContentVisibility(false); 2048 } 2049 }) 2050 .start(); 2051 } 2052 animateSwitchBubbles()2053 private void animateSwitchBubbles() { 2054 // If we're no longer expanded, this is meaningless. 2055 if (!mIsExpanded) { 2056 return; 2057 } 2058 2059 mIsBubbleSwitchAnimating = true; 2060 2061 // The surface contains a screenshot of the animating out bubble, so we just need to animate 2062 // it out (and then release the GraphicBuffer). 2063 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2064 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) 2065 .spring(DynamicAnimation.SCALE_X, 0f, mScaleOutSpringConfig) 2066 .spring(DynamicAnimation.SCALE_Y, 0f, mScaleOutSpringConfig) 2067 .spring(DynamicAnimation.TRANSLATION_Y, 2068 mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize * 2, 2069 mTranslateSpringConfig) 2070 .withEndActions(this::releaseAnimatingOutBubbleBuffer) 2071 .start(); 2072 2073 boolean isOverflow = mExpandedBubble != null 2074 && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); 2075 float expandingFromBubbleDestinationX = 2076 mExpandedAnimationController.getBubbleLeft(isOverflow ? getBubbleCount() 2077 : mBubbleData.getBubbles().indexOf(mExpandedBubble)); 2078 2079 mExpandedViewContainer.setAlpha(1f); 2080 mExpandedViewContainer.setVisibility(View.VISIBLE); 2081 2082 mExpandedViewContainerMatrix.setScale( 2083 0f, 0f, expandingFromBubbleDestinationX + mBubbleSize / 2f, getExpandedViewY()); 2084 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2085 2086 mDelayedAnimationHandler.postDelayed(() -> { 2087 if (!mIsExpanded) { 2088 mIsBubbleSwitchAnimating = false; 2089 return; 2090 } 2091 2092 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2093 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2094 .spring(AnimatableScaleMatrix.SCALE_X, 2095 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2096 mScaleInSpringConfig) 2097 .spring(AnimatableScaleMatrix.SCALE_Y, 2098 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2099 mScaleInSpringConfig) 2100 .addUpdateListener((target, values) -> { 2101 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2102 }) 2103 .withEndActions(() -> { 2104 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 2105 mExpandedBubble.getExpandedView().setContentVisibility(true); 2106 mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false); 2107 } 2108 2109 mIsBubbleSwitchAnimating = false; 2110 }) 2111 .start(); 2112 }, 25); 2113 } 2114 2115 /** 2116 * Cancels any delayed steps for expand/collapse and bubble switch animations, and resets the is 2117 * animating flags for those animations. 2118 */ cancelDelayedExpandCollapseSwitchAnimations()2119 private void cancelDelayedExpandCollapseSwitchAnimations() { 2120 mDelayedAnimationHandler.removeCallbacksAndMessages(null); 2121 2122 mIsExpansionAnimating = false; 2123 mIsBubbleSwitchAnimating = false; 2124 } 2125 cancelAllExpandCollapseSwitchAnimations()2126 private void cancelAllExpandCollapseSwitchAnimations() { 2127 cancelDelayedExpandCollapseSwitchAnimations(); 2128 2129 PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel(); 2130 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2131 2132 mExpandedViewContainer.setAnimationMatrix(null); 2133 } 2134 notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded)2135 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) { 2136 if (mExpandListener != null && bubble != null) { 2137 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey()); 2138 } 2139 } 2140 2141 /** Return the BubbleView at the given index from the bubble container. */ getBubbleAt(int i)2142 public BadgedImageView getBubbleAt(int i) { 2143 return getBubbleCount() > i 2144 ? (BadgedImageView) mBubbleContainer.getChildAt(i) 2145 : null; 2146 } 2147 2148 /** Moves the bubbles out of the way if they're going to be over the keyboard. */ onImeVisibilityChanged(boolean visible, int height)2149 public void onImeVisibilityChanged(boolean visible, int height) { 2150 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); 2151 2152 if (!mIsExpanded && getBubbleCount() > 0) { 2153 final float stackDestinationY = 2154 mStackAnimationController.animateForImeVisibility(visible); 2155 2156 // How far the stack is animating due to IME, we'll just animate the flyout by that 2157 // much too. 2158 final float stackDy = 2159 stackDestinationY - mStackAnimationController.getStackPosition().y; 2160 2161 // If the flyout is visible, translate it along with the bubble stack. 2162 if (mFlyout.getVisibility() == VISIBLE) { 2163 PhysicsAnimator.getInstance(mFlyout) 2164 .spring(DynamicAnimation.TRANSLATION_Y, 2165 mFlyout.getTranslationY() + stackDy, 2166 FLYOUT_IME_ANIMATION_SPRING_CONFIG) 2167 .start(); 2168 } 2169 } 2170 } 2171 2172 /** 2173 * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a 2174 * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV). 2175 * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided 2176 * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to 2177 * the special nature of ActivityView, it does not respect the standard 2178 * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for 2179 * this purpose. 2180 * 2181 * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation 2182 * properties for performance reasons. This means that the default implementation of this method 2183 * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in 2184 * it not receiving any touch events. This was previously addressed by returning false in the 2185 * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any 2186 * touch handlers in the stack or its child views. 2187 * 2188 * To support touch handlers, we're overriding this method to leave the ActivityView's touchable 2189 * region alone. The only touchable part of the stack that can ever overlap the AV is a 2190 * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually 2191 * updating the touchable region to allow users to grab a bubble while it completes its ~50ms 2192 * animation back to the bubble row. 2193 * 2194 * NOTE: Any future additions to the stack that obscure the ActivityView region will need their 2195 * bounds subtracted here in order to receive touch events. 2196 */ 2197 @Override subtractObscuredTouchableRegion(Region touchableRegion, View view)2198 public void subtractObscuredTouchableRegion(Region touchableRegion, View view) { 2199 // If the notification shade is expanded, or the manage menu is open, or we are showing 2200 // manage bubbles user education, we shouldn't let the ActivityView steal any touch events 2201 // from any location. 2202 if (!mIsExpanded 2203 || mShowingManage 2204 || (mManageEducationView != null 2205 && mManageEducationView.getVisibility() == VISIBLE)) { 2206 touchableRegion.setEmpty(); 2207 } 2208 } 2209 2210 /** 2211 * If you're here because you're not receiving touch events on a view that is a descendant of 2212 * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the 2213 * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView 2214 * consumes all touch events within its bounds, even for views like the BubbleStackView that are 2215 * above it. It ignores typical view touch handling methods like this one and 2216 * dispatchTouchEvent. 2217 */ 2218 @Override onInterceptTouchEvent(MotionEvent ev)2219 public boolean onInterceptTouchEvent(MotionEvent ev) { 2220 return super.onInterceptTouchEvent(ev); 2221 } 2222 2223 @Override dispatchTouchEvent(MotionEvent ev)2224 public boolean dispatchTouchEvent(MotionEvent ev) { 2225 if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { 2226 // Ignore touches from additional pointer indices. 2227 return false; 2228 } 2229 2230 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 2231 mPointerIndexDown = ev.getActionIndex(); 2232 } else if (ev.getAction() == MotionEvent.ACTION_UP 2233 || ev.getAction() == MotionEvent.ACTION_CANCEL) { 2234 mPointerIndexDown = -1; 2235 } 2236 2237 boolean dispatched = super.dispatchTouchEvent(ev); 2238 2239 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned 2240 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will 2241 // then be passed to the new bubble, which will not consume them since it hasn't received an 2242 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler 2243 // until the current gesture ends with an ACTION_UP event. 2244 if (!dispatched && !mIsExpanded && mIsGestureInProgress) { 2245 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev); 2246 } 2247 2248 mIsGestureInProgress = 2249 ev.getAction() != MotionEvent.ACTION_UP 2250 && ev.getAction() != MotionEvent.ACTION_CANCEL; 2251 2252 return dispatched; 2253 } 2254 setFlyoutStateForDragLength(float deltaX)2255 void setFlyoutStateForDragLength(float deltaX) { 2256 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method 2257 // is continually called. 2258 if (mFlyout.getWidth() <= 0) { 2259 return; 2260 } 2261 2262 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 2263 mFlyoutDragDeltaX = deltaX; 2264 2265 final float collapsePercent = 2266 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); 2267 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); 2268 2269 // Calculate how to translate the flyout if it has been dragged too far in either direction. 2270 float overscrollTranslation = 0f; 2271 if (collapsePercent < 0f || collapsePercent > 1f) { 2272 // Whether we are more than 100% transitioned to the dot. 2273 final boolean overscrollingPastDot = collapsePercent > 1f; 2274 2275 // Whether we are overscrolling physically to the left - this can either be pulling the 2276 // flyout away from the stack (if the stack is on the right) or pushing it to the left 2277 // after it has already become the dot. 2278 final boolean overscrollingLeft = 2279 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); 2280 overscrollTranslation = 2281 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) 2282 * (overscrollingLeft ? -1 : 1) 2283 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR 2284 // Attenuate the smaller dot less than the larger flyout. 2285 / (overscrollingPastDot ? 2 : 1))); 2286 } 2287 2288 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); 2289 } 2290 2291 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */ passEventToMagnetizedObject(MotionEvent event)2292 private boolean passEventToMagnetizedObject(MotionEvent event) { 2293 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); 2294 } 2295 2296 /** 2297 * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the 2298 * stack, if we're collapsed. 2299 */ dismissMagnetizedObject()2300 private void dismissMagnetizedObject() { 2301 if (mIsExpanded) { 2302 final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); 2303 dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); 2304 } else { 2305 mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE); 2306 } 2307 } 2308 dismissBubbleIfExists(@ullable Bubble bubble)2309 private void dismissBubbleIfExists(@Nullable Bubble bubble) { 2310 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 2311 mBubbleData.dismissBubbleWithKey( 2312 bubble.getKey(), BubbleController.DISMISS_USER_GESTURE); 2313 } 2314 } 2315 2316 /** Prepares and starts the desaturate/darken animation on the bubble stack. */ animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken)2317 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { 2318 mDesaturateAndDarkenTargetView = targetView; 2319 2320 if (mDesaturateAndDarkenTargetView == null) { 2321 return; 2322 } 2323 2324 if (desaturateAndDarken) { 2325 // Use the animated paint for the bubbles. 2326 mDesaturateAndDarkenTargetView.setLayerType( 2327 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint); 2328 mDesaturateAndDarkenAnimator.removeAllListeners(); 2329 mDesaturateAndDarkenAnimator.start(); 2330 } else { 2331 mDesaturateAndDarkenAnimator.removeAllListeners(); 2332 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() { 2333 @Override 2334 public void onAnimationEnd(Animator animation) { 2335 super.onAnimationEnd(animation); 2336 // Stop using the animated paint. 2337 resetDesaturationAndDarken(); 2338 } 2339 }); 2340 mDesaturateAndDarkenAnimator.reverse(); 2341 } 2342 } 2343 resetDesaturationAndDarken()2344 private void resetDesaturationAndDarken() { 2345 2346 mDesaturateAndDarkenAnimator.removeAllListeners(); 2347 mDesaturateAndDarkenAnimator.cancel(); 2348 2349 if (mDesaturateAndDarkenTargetView != null) { 2350 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null); 2351 mDesaturateAndDarkenTargetView = null; 2352 } 2353 } 2354 2355 /** Animates in the dismiss target. */ springInDismissTargetMaybe()2356 private void springInDismissTargetMaybe() { 2357 if (mShowingDismiss) { 2358 return; 2359 } 2360 2361 mShowingDismiss = true; 2362 2363 mDismissTargetContainer.bringToFront(); 2364 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1); 2365 mDismissTargetContainer.setVisibility(VISIBLE); 2366 2367 ((TransitionDrawable) mDismissTargetContainer.getBackground()).startTransition( 2368 DISMISS_TRANSITION_DURATION_MS); 2369 2370 mDismissTargetAnimator.cancel(); 2371 mDismissTargetAnimator 2372 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring) 2373 .start(); 2374 } 2375 2376 /** 2377 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they 2378 * were dragged into the target and encircled. 2379 */ hideDismissTarget()2380 private void hideDismissTarget() { 2381 if (!mShowingDismiss) { 2382 return; 2383 } 2384 2385 mShowingDismiss = false; 2386 2387 ((TransitionDrawable) mDismissTargetContainer.getBackground()).reverseTransition( 2388 DISMISS_TRANSITION_DURATION_MS); 2389 2390 mDismissTargetAnimator 2391 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(), 2392 mDismissTargetSpring) 2393 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE)) 2394 .start(); 2395 } 2396 2397 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ animateFlyoutCollapsed(boolean collapsed, float velX)2398 private void animateFlyoutCollapsed(boolean collapsed, float velX) { 2399 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 2400 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's 2401 // faster. 2402 mFlyoutTransitionSpring.getSpring().setStiffness( 2403 (mBubbleToExpandAfterFlyoutCollapse != null) 2404 ? SpringForce.STIFFNESS_MEDIUM 2405 : SpringForce.STIFFNESS_LOW); 2406 mFlyoutTransitionSpring 2407 .setStartValue(mFlyoutDragDeltaX) 2408 .setStartVelocity(velX) 2409 .animateToFinalPosition(collapsed 2410 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) 2411 : 0f); 2412 } 2413 2414 /** 2415 * Calculates the y position of the expanded view when it is expanded. 2416 */ getExpandedViewY()2417 float getExpandedViewY() { 2418 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop; 2419 } 2420 2421 /** 2422 * Animates in the flyout for the given bubble, if available, and then hides it after some time. 2423 */ 2424 @VisibleForTesting animateInFlyoutForBubble(Bubble bubble)2425 void animateInFlyoutForBubble(Bubble bubble) { 2426 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); 2427 final BadgedImageView bubbleView = bubble.getIconView(); 2428 if (flyoutMessage == null 2429 || flyoutMessage.message == null 2430 || !bubble.showFlyout() 2431 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) 2432 || isExpanded() 2433 || mIsExpansionAnimating 2434 || mIsGestureInProgress 2435 || mBubbleToExpandAfterFlyoutCollapse != null 2436 || bubbleView == null) { 2437 if (bubbleView != null) { 2438 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 2439 } 2440 // Skip the message if none exists, we're expanded or animating expansion, or we're 2441 // about to expand a bubble from the previous tapped flyout, or if bubble view is null. 2442 return; 2443 } 2444 2445 mFlyoutDragDeltaX = 0f; 2446 clearFlyoutOnHide(); 2447 mAfterFlyoutHidden = () -> { 2448 // Null it out to ensure it runs once. 2449 mAfterFlyoutHidden = null; 2450 2451 if (mBubbleToExpandAfterFlyoutCollapse != null) { 2452 // User tapped on the flyout and we should expand 2453 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); 2454 mBubbleData.setExpanded(true); 2455 mBubbleToExpandAfterFlyoutCollapse = null; 2456 } 2457 2458 // Stop suppressing the dot now that the flyout has morphed into the dot. 2459 bubbleView.removeDotSuppressionFlag( 2460 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 2461 2462 mFlyout.setVisibility(INVISIBLE); 2463 2464 // Hide the stack after a delay, if needed. 2465 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 2466 }; 2467 mFlyout.setVisibility(INVISIBLE); 2468 2469 // Suppress the dot when we are animating the flyout. 2470 bubbleView.addDotSuppressionFlag( 2471 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 2472 2473 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0. 2474 post(() -> { 2475 // An auto-expanding bubble could have been posted during the time it takes to 2476 // layout. 2477 if (isExpanded()) { 2478 return; 2479 } 2480 final Runnable expandFlyoutAfterDelay = () -> { 2481 mAnimateInFlyout = () -> { 2482 mFlyout.setVisibility(VISIBLE); 2483 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 2484 mFlyoutDragDeltaX = 2485 mStackAnimationController.isStackOnLeftSide() 2486 ? -mFlyout.getWidth() 2487 : mFlyout.getWidth(); 2488 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); 2489 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 2490 }; 2491 mFlyout.postDelayed(mAnimateInFlyout, 200); 2492 }; 2493 2494 if (bubble.getIconView() == null) { 2495 return; 2496 } 2497 2498 mFlyout.setupFlyoutStartingAsDot(flyoutMessage, 2499 mStackAnimationController.getStackPosition(), getWidth(), 2500 mStackAnimationController.isStackOnLeftSide(), 2501 bubble.getIconView().getDotColor() /* dotColor */, 2502 expandFlyoutAfterDelay /* onLayoutComplete */, 2503 mAfterFlyoutHidden, 2504 bubble.getIconView().getDotCenter(), 2505 !bubble.showDot()); 2506 mFlyout.bringToFront(); 2507 }); 2508 mFlyout.removeCallbacks(mHideFlyout); 2509 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 2510 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); 2511 } 2512 2513 /** Hide the flyout immediately and cancel any pending hide runnables. */ hideFlyoutImmediate()2514 private void hideFlyoutImmediate() { 2515 clearFlyoutOnHide(); 2516 mFlyout.removeCallbacks(mAnimateInFlyout); 2517 mFlyout.removeCallbacks(mHideFlyout); 2518 mFlyout.hideFlyout(); 2519 } 2520 clearFlyoutOnHide()2521 private void clearFlyoutOnHide() { 2522 mFlyout.removeCallbacks(mAnimateInFlyout); 2523 if (mAfterFlyoutHidden == null) { 2524 return; 2525 } 2526 mAfterFlyoutHidden.run(); 2527 mAfterFlyoutHidden = null; 2528 } 2529 2530 /** 2531 * Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager 2532 * to decide which touch events go to Bubbles. 2533 * 2534 * Bubbles is below the status bar/notification shade but above application windows. If you're 2535 * trying to get touch events from the status bar or another higher-level window layer, you'll 2536 * need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal 2537 * them. 2538 */ getTouchableRegion(Rect outRect)2539 public void getTouchableRegion(Rect outRect) { 2540 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) { 2541 // When user education shows then capture all touches 2542 outRect.set(0, 0, getWidth(), getHeight()); 2543 return; 2544 } 2545 2546 if (!mIsExpanded) { 2547 if (getBubbleCount() > 0) { 2548 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); 2549 // Increase the touch target size of the bubble 2550 outRect.top -= mBubbleTouchPadding; 2551 outRect.left -= mBubbleTouchPadding; 2552 outRect.right += mBubbleTouchPadding; 2553 outRect.bottom += mBubbleTouchPadding; 2554 } 2555 } else { 2556 mBubbleContainer.getBoundsOnScreen(outRect); 2557 } 2558 2559 if (mFlyout.getVisibility() == View.VISIBLE) { 2560 final Rect flyoutBounds = new Rect(); 2561 mFlyout.getBoundsOnScreen(flyoutBounds); 2562 outRect.union(flyoutBounds); 2563 } 2564 } 2565 getStatusBarHeight()2566 private int getStatusBarHeight() { 2567 if (getRootWindowInsets() != null) { 2568 WindowInsets insets = getRootWindowInsets(); 2569 return Math.max( 2570 mStatusBarHeight, 2571 insets.getDisplayCutout() != null 2572 ? insets.getDisplayCutout().getSafeInsetTop() 2573 : 0); 2574 } 2575 2576 return 0; 2577 } 2578 requestUpdate()2579 private void requestUpdate() { 2580 if (mViewUpdatedRequested || mIsExpansionAnimating) { 2581 return; 2582 } 2583 mViewUpdatedRequested = true; 2584 getViewTreeObserver().addOnPreDrawListener(mViewUpdater); 2585 invalidate(); 2586 } 2587 showManageMenu(boolean show)2588 private void showManageMenu(boolean show) { 2589 mShowingManage = show; 2590 2591 // This should not happen, since the manage menu is only visible when there's an expanded 2592 // bubble. If we end up in this state, just hide the menu immediately. 2593 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2594 mManageMenu.setVisibility(View.INVISIBLE); 2595 return; 2596 } 2597 2598 // If available, update the manage menu's settings option with the expanded bubble's app 2599 // name and icon. 2600 if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) { 2601 final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); 2602 mManageSettingsIcon.setImageDrawable(bubble.getBadgedAppIcon()); 2603 mManageSettingsText.setText(getResources().getString( 2604 R.string.bubbles_app_settings, bubble.getAppName())); 2605 } 2606 2607 mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); 2608 2609 final boolean isLtr = 2610 getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR; 2611 2612 // When the menu is open, it should be at these coordinates. The menu pops out to the right 2613 // in LTR and to the left in RTL. 2614 final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth(); 2615 final float targetY = mTempRect.bottom - mManageMenu.getHeight(); 2616 2617 final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; 2618 2619 if (show) { 2620 mManageMenu.setScaleX(0.5f); 2621 mManageMenu.setScaleY(0.5f); 2622 mManageMenu.setTranslationX(targetX - xOffsetForAnimation); 2623 mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f); 2624 mManageMenu.setAlpha(0f); 2625 2626 PhysicsAnimator.getInstance(mManageMenu) 2627 .spring(DynamicAnimation.ALPHA, 1f) 2628 .spring(DynamicAnimation.SCALE_X, 1f) 2629 .spring(DynamicAnimation.SCALE_Y, 1f) 2630 .spring(DynamicAnimation.TRANSLATION_X, targetX) 2631 .spring(DynamicAnimation.TRANSLATION_Y, targetY) 2632 .withEndActions(() -> { 2633 View child = mManageMenu.getChildAt(0); 2634 child.requestAccessibilityFocus(); 2635 }) 2636 .start(); 2637 2638 mManageMenu.setVisibility(View.VISIBLE); 2639 } else { 2640 PhysicsAnimator.getInstance(mManageMenu) 2641 .spring(DynamicAnimation.ALPHA, 0f) 2642 .spring(DynamicAnimation.SCALE_X, 0.5f) 2643 .spring(DynamicAnimation.SCALE_Y, 0.5f) 2644 .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation) 2645 .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f) 2646 .withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE)) 2647 .start(); 2648 } 2649 2650 // Update the AV's obscured touchable region for the new menu visibility state. 2651 mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); 2652 } 2653 updateExpandedBubble()2654 private void updateExpandedBubble() { 2655 if (DEBUG_BUBBLE_STACK_VIEW) { 2656 Log.d(TAG, "updateExpandedBubble()"); 2657 } 2658 2659 mExpandedViewContainer.removeAllViews(); 2660 if (mIsExpanded && mExpandedBubble != null 2661 && mExpandedBubble.getExpandedView() != null) { 2662 BubbleExpandedView bev = mExpandedBubble.getExpandedView(); 2663 bev.setContentVisibility(false); 2664 mExpandedViewContainerMatrix.setScaleX(0f); 2665 mExpandedViewContainerMatrix.setScaleY(0f); 2666 mExpandedViewContainerMatrix.setTranslate(0f, 0f); 2667 mExpandedViewContainer.setVisibility(View.INVISIBLE); 2668 mExpandedViewContainer.setAlpha(0f); 2669 mExpandedViewContainer.addView(bev); 2670 bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); 2671 bev.populateExpandedView(); 2672 2673 if (!mIsExpansionAnimating) { 2674 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 2675 post(this::animateSwitchBubbles); 2676 }); 2677 } 2678 } 2679 } 2680 2681 /** 2682 * Requests a snapshot from the currently expanded bubble's ActivityView and displays it in a 2683 * SurfaceView. This allows us to load a newly expanded bubble's Activity into the ActivityView, 2684 * while animating the (screenshot of the) previously selected bubble's content away. 2685 * 2686 * @param onComplete Callback to run once we're done here - called with 'false' if something 2687 * went wrong, or 'true' if the SurfaceView is now showing a screenshot of the 2688 * expanded bubble. 2689 */ screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete)2690 private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) { 2691 if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2692 // You can't animate null. 2693 onComplete.accept(false); 2694 return; 2695 } 2696 2697 final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView(); 2698 2699 // Release the previous screenshot if it hasn't been released already. 2700 if (mAnimatingOutBubbleBuffer != null) { 2701 releaseAnimatingOutBubbleBuffer(); 2702 } 2703 2704 try { 2705 mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface(); 2706 } catch (Exception e) { 2707 // If we fail for any reason, print the stack trace and then notify the callback of our 2708 // failure. This is not expected to occur, but it's not worth crashing over. 2709 Log.wtf(TAG, e); 2710 onComplete.accept(false); 2711 } 2712 2713 if (mAnimatingOutBubbleBuffer == null 2714 || mAnimatingOutBubbleBuffer.getGraphicBuffer() == null) { 2715 // While no exception was thrown, we were unable to get a snapshot. 2716 onComplete.accept(false); 2717 return; 2718 } 2719 2720 // Make sure the surface container's properties have been reset. 2721 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2722 mAnimatingOutSurfaceContainer.setScaleX(1f); 2723 mAnimatingOutSurfaceContainer.setScaleY(1f); 2724 mAnimatingOutSurfaceContainer.setTranslationX(0); 2725 mAnimatingOutSurfaceContainer.setTranslationY(0); 2726 2727 final int[] activityViewLocation = 2728 mExpandedBubble.getExpandedView().getActivityViewLocationOnScreen(); 2729 final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); 2730 2731 // Translate the surface to overlap the real ActivityView. 2732 mAnimatingOutSurfaceContainer.setTranslationY( 2733 activityViewLocation[1] - surfaceViewLocation[1]); 2734 2735 // Set the width/height of the SurfaceView to match the snapshot. 2736 mAnimatingOutSurfaceView.getLayoutParams().width = 2737 mAnimatingOutBubbleBuffer.getGraphicBuffer().getWidth(); 2738 mAnimatingOutSurfaceView.getLayoutParams().height = 2739 mAnimatingOutBubbleBuffer.getGraphicBuffer().getHeight(); 2740 mAnimatingOutSurfaceView.requestLayout(); 2741 2742 // Post to wait for layout. 2743 post(() -> { 2744 // The buffer might have been destroyed if the user is mashing on bubbles, that's okay. 2745 if (mAnimatingOutBubbleBuffer.getGraphicBuffer().isDestroyed()) { 2746 onComplete.accept(false); 2747 return; 2748 } 2749 2750 if (!mIsExpanded) { 2751 onComplete.accept(false); 2752 return; 2753 } 2754 2755 // Attach the buffer! We're now displaying the snapshot. 2756 mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace( 2757 mAnimatingOutBubbleBuffer.getGraphicBuffer(), 2758 mAnimatingOutBubbleBuffer.getColorSpace()); 2759 2760 mSurfaceSynchronizer.syncSurfaceAndRun(() -> post(() -> onComplete.accept(true))); 2761 }); 2762 } 2763 2764 /** 2765 * Releases the buffer containing the screenshot of the animating-out bubble, if it exists and 2766 * isn't yet destroyed. 2767 */ releaseAnimatingOutBubbleBuffer()2768 private void releaseAnimatingOutBubbleBuffer() { 2769 if (mAnimatingOutBubbleBuffer != null 2770 && !mAnimatingOutBubbleBuffer.getGraphicBuffer().isDestroyed()) { 2771 mAnimatingOutBubbleBuffer.getGraphicBuffer().destroy(); 2772 } 2773 } 2774 updateExpandedView()2775 private void updateExpandedView() { 2776 if (DEBUG_BUBBLE_STACK_VIEW) { 2777 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded); 2778 } 2779 2780 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); 2781 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { 2782 mExpandedViewContainer.setTranslationY(getExpandedViewY()); 2783 mExpandedBubble.getExpandedView().updateView( 2784 mExpandedViewContainer.getLocationOnScreen()); 2785 } 2786 2787 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 2788 updateBubbleZOrdersAndDotPosition(false); 2789 } 2790 2791 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */ updateBubbleZOrdersAndDotPosition(boolean animate)2792 private void updateBubbleZOrdersAndDotPosition(boolean animate) { 2793 int bubbleCount = getBubbleCount(); 2794 for (int i = 0; i < bubbleCount; i++) { 2795 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 2796 bv.setZ((mMaxBubbles * mBubbleElevation) - i); 2797 2798 // If the dot is on the left, and so is the stack, we need to change the dot position. 2799 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) { 2800 bv.setDotPositionOnLeft(!mStackOnLeftOrWillBe, animate); 2801 } 2802 2803 if (!mIsExpanded && i > 0) { 2804 // If we're collapsed and this bubble is behind other bubbles, suppress its dot. 2805 bv.addDotSuppressionFlag( 2806 BadgedImageView.SuppressionFlag.BEHIND_STACK); 2807 } else { 2808 bv.removeDotSuppressionFlag( 2809 BadgedImageView.SuppressionFlag.BEHIND_STACK); 2810 } 2811 } 2812 } 2813 updatePointerPosition()2814 private void updatePointerPosition() { 2815 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2816 return; 2817 } 2818 int index = getBubbleIndex(mExpandedBubble); 2819 if (index == -1) { 2820 return; 2821 } 2822 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index); 2823 float halfBubble = mBubbleSize / 2f; 2824 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble; 2825 // Padding might be adjusted for insets, so get it directly from the view 2826 bubbleCenter -= mExpandedViewContainer.getPaddingLeft(); 2827 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter); 2828 } 2829 2830 /** 2831 * @return the number of bubbles in the stack view. 2832 */ getBubbleCount()2833 public int getBubbleCount() { 2834 // Subtract 1 for the overflow button that is always in the bubble container. 2835 return mBubbleContainer.getChildCount() - 1; 2836 } 2837 2838 /** 2839 * Finds the bubble index within the stack. 2840 * 2841 * @param provider the bubble view provider with the bubble to look up. 2842 * @return the index of the bubble view within the bubble stack. The range of the position 2843 * is between 0 and the bubble count minus 1. 2844 */ getBubbleIndex(@ullable BubbleViewProvider provider)2845 int getBubbleIndex(@Nullable BubbleViewProvider provider) { 2846 if (provider == null) { 2847 return 0; 2848 } 2849 return mBubbleContainer.indexOfChild(provider.getIconView()); 2850 } 2851 2852 /** 2853 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places. 2854 */ getNormalizedXPosition()2855 public float getNormalizedXPosition() { 2856 return new BigDecimal(getStackPosition().x / mDisplaySize.x) 2857 .setScale(4, RoundingMode.CEILING.HALF_UP) 2858 .floatValue(); 2859 } 2860 2861 /** 2862 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places. 2863 */ getNormalizedYPosition()2864 public float getNormalizedYPosition() { 2865 return new BigDecimal(getStackPosition().y / mDisplaySize.y) 2866 .setScale(4, RoundingMode.CEILING.HALF_UP) 2867 .floatValue(); 2868 } 2869 setStackStartPosition(RelativeStackPosition position)2870 public void setStackStartPosition(RelativeStackPosition position) { 2871 mStackAnimationController.setStackStartPosition(position); 2872 } 2873 getStackPosition()2874 public PointF getStackPosition() { 2875 return mStackAnimationController.getStackPosition(); 2876 } 2877 getRelativeStackPosition()2878 public RelativeStackPosition getRelativeStackPosition() { 2879 return mStackAnimationController.getRelativeStackPosition(); 2880 } 2881 2882 /** 2883 * Logs the bubble UI event. 2884 * 2885 * @param provider the bubble view provider that is being interacted on. Null value indicates 2886 * that the user interaction is not specific to one bubble. 2887 * @param action the user interaction enum. 2888 */ logBubbleEvent(@ullable BubbleViewProvider provider, int action)2889 private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { 2890 if (provider == null || provider.getKey().equals(BubbleOverflow.KEY)) { 2891 SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, 2892 mContext.getApplicationInfo().packageName, 2893 provider == null ? null : BubbleOverflow.KEY /* notification channel */, 2894 0 /* notification ID */, 2895 0 /* bubble position */, 2896 getBubbleCount(), 2897 action, 2898 getNormalizedXPosition(), 2899 getNormalizedYPosition(), 2900 false /* unread bubble */, 2901 false /* on-going bubble */, 2902 false /* isAppForeground (unused) */); 2903 return; 2904 } 2905 provider.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(), 2906 getNormalizedYPosition(), getBubbleIndex(provider)); 2907 } 2908 2909 /** 2910 * Called when a back gesture should be directed to the Bubbles stack. When expanded, 2911 * a back key down/up event pair is forwarded to the bubble Activity. 2912 */ performBackPressIfNeeded()2913 boolean performBackPressIfNeeded() { 2914 if (!isExpanded() || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { 2915 return false; 2916 } 2917 return mExpandedBubble.getExpandedView().performBackPressIfNeeded(); 2918 } 2919 2920 /** Whether the educational view should appear for bubbles. **/ shouldShowBubblesEducation()2921 private boolean shouldShowBubblesEducation() { 2922 return BubbleDebugConfig.forceShowUserEducation(getContext()) 2923 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false); 2924 } 2925 2926 /** Whether the educational view should appear for the expanded view "manage" button. **/ shouldShowManageEducation()2927 private boolean shouldShowManageEducation() { 2928 return BubbleDebugConfig.forceShowUserEducation(getContext()) 2929 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false); 2930 } 2931 2932 /** For debugging only */ getBubblesOnScreen()2933 List<Bubble> getBubblesOnScreen() { 2934 List<Bubble> bubbles = new ArrayList<>(); 2935 for (int i = 0; i < getBubbleCount(); i++) { 2936 View child = mBubbleContainer.getChildAt(i); 2937 if (child instanceof BadgedImageView) { 2938 String key = ((BadgedImageView) child).getKey(); 2939 Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); 2940 bubbles.add(bubble); 2941 } 2942 } 2943 return bubbles; 2944 } 2945 2946 /** 2947 * Representation of stack position that uses relative properties rather than absolute 2948 * coordinates. This is used to maintain similar stack positions across configuration changes. 2949 */ 2950 public static class RelativeStackPosition { 2951 /** Whether to place the stack at the leftmost allowed position. */ 2952 private boolean mOnLeft; 2953 2954 /** 2955 * How far down the vertically allowed region to place the stack. For example, if the stack 2956 * allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at 2957 * 100 + (0.2f * 1000) = 300. 2958 */ 2959 private float mVerticalOffsetPercent; 2960 RelativeStackPosition(boolean onLeft, float verticalOffsetPercent)2961 public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) { 2962 mOnLeft = onLeft; 2963 mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent); 2964 } 2965 2966 /** Constructs a relative position given a region and a point in that region. */ RelativeStackPosition(PointF position, RectF region)2967 public RelativeStackPosition(PointF position, RectF region) { 2968 mOnLeft = position.x < region.width() / 2; 2969 mVerticalOffsetPercent = 2970 clampVerticalOffsetPercent((position.y - region.top) / region.height()); 2971 } 2972 2973 /** Ensures that the offset percent is between 0f and 1f. */ 2974 private float clampVerticalOffsetPercent(float offsetPercent) { 2975 return Math.max(0f, Math.min(1f, offsetPercent)); 2976 } 2977 2978 /** 2979 * Given an allowable stack position region, returns the point within that region 2980 * represented by this relative position. 2981 */ 2982 public PointF getAbsolutePositionInRegion(RectF region) { 2983 return new PointF( 2984 mOnLeft ? region.left : region.right, 2985 region.top + mVerticalOffsetPercent * region.height()); 2986 } 2987 } 2988 } 2989