1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.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.wm.shell.animation.Interpolators.ALPHA_IN; 23 import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; 24 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 25 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 26 import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RESTING; 27 import static com.android.wm.shell.bubbles.BubblePositioner.StackPinnedEdge.LEFT; 28 import static com.android.wm.shell.bubbles.BubblePositioner.StackPinnedEdge.RIGHT; 29 import static com.android.wm.shell.common.bubbles.BubbleConstants.BUBBLE_EXPANDED_SCRIM_ALPHA; 30 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; 31 32 import android.animation.Animator; 33 import android.animation.AnimatorListenerAdapter; 34 import android.animation.AnimatorSet; 35 import android.animation.ObjectAnimator; 36 import android.animation.ValueAnimator; 37 import android.annotation.SuppressLint; 38 import android.content.ContentResolver; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.res.Resources; 42 import android.content.res.TypedArray; 43 import android.graphics.Color; 44 import android.graphics.Outline; 45 import android.graphics.PointF; 46 import android.graphics.PorterDuff; 47 import android.graphics.Rect; 48 import android.graphics.RectF; 49 import android.graphics.drawable.ColorDrawable; 50 import android.os.Bundle; 51 import android.provider.Settings; 52 import android.util.Log; 53 import android.view.Choreographer; 54 import android.view.LayoutInflater; 55 import android.view.MotionEvent; 56 import android.view.SurfaceHolder; 57 import android.view.SurfaceView; 58 import android.view.View; 59 import android.view.ViewGroup; 60 import android.view.ViewOutlineProvider; 61 import android.view.ViewPropertyAnimator; 62 import android.view.ViewTreeObserver; 63 import android.view.WindowManager; 64 import android.view.WindowManagerPolicyConstants; 65 import android.view.accessibility.AccessibilityNodeInfo; 66 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 67 import android.widget.FrameLayout; 68 import android.widget.ImageView; 69 import android.widget.TextView; 70 import android.window.ScreenCapture; 71 72 import androidx.annotation.NonNull; 73 import androidx.annotation.Nullable; 74 import androidx.dynamicanimation.animation.DynamicAnimation; 75 import androidx.dynamicanimation.animation.FloatPropertyCompat; 76 import androidx.dynamicanimation.animation.SpringAnimation; 77 import androidx.dynamicanimation.animation.SpringForce; 78 79 import com.android.internal.annotations.VisibleForTesting; 80 import com.android.internal.policy.ScreenDecorationsUtils; 81 import com.android.internal.protolog.common.ProtoLog; 82 import com.android.internal.util.FrameworkStatsLog; 83 import com.android.wm.shell.Flags; 84 import com.android.wm.shell.R; 85 import com.android.wm.shell.animation.Interpolators; 86 import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener; 87 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; 88 import com.android.wm.shell.bubbles.animation.ExpandedAnimationController; 89 import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationController; 90 import com.android.wm.shell.bubbles.animation.ExpandedViewAnimationControllerImpl; 91 import com.android.wm.shell.bubbles.animation.PhysicsAnimationLayout; 92 import com.android.wm.shell.bubbles.animation.StackAnimationController; 93 import com.android.wm.shell.common.FloatingContentCoordinator; 94 import com.android.wm.shell.common.ShellExecutor; 95 import com.android.wm.shell.common.bubbles.DismissView; 96 import com.android.wm.shell.common.bubbles.RelativeTouchListener; 97 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 98 import com.android.wm.shell.shared.animation.PhysicsAnimator; 99 100 import java.io.PrintWriter; 101 import java.math.BigDecimal; 102 import java.math.RoundingMode; 103 import java.util.ArrayList; 104 import java.util.Collections; 105 import java.util.List; 106 import java.util.Objects; 107 import java.util.function.Consumer; 108 import java.util.stream.Collectors; 109 110 /** 111 * Renders bubbles in a stack and handles animating expanded and collapsed states. 112 */ 113 public class BubbleStackView extends FrameLayout 114 implements ViewTreeObserver.OnComputeInternalInsetsListener { 115 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES; 116 117 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */ 118 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f; 119 120 /** Velocity required to dismiss the flyout via drag. */ 121 private static final float FLYOUT_DISMISS_VELOCITY = 2000f; 122 123 /** 124 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel 125 * for every 8 pixels overscrolled). 126 */ 127 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; 128 129 private static final int FADE_IN_DURATION = 320; 130 131 /** How long to wait, in milliseconds, before hiding the flyout. */ 132 @VisibleForTesting 133 static final int FLYOUT_HIDE_AFTER = 5000; 134 135 private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; 136 137 private static final float OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT = 0.5f; 138 139 private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; 140 141 /** Minimum alpha value for scrim when alpha is being changed via drag */ 142 private static final float MIN_SCRIM_ALPHA_FOR_DRAG = 0.2f; 143 144 /** 145 * How long to wait to animate the stack temporarily invisible after a drag/flyout hide 146 * animation ends, if we are in fact temporarily invisible. 147 */ 148 private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000; 149 150 /** 151 * Percent of the bubble that is hidden while stashed. 152 */ 153 private static final float PERCENT_HIDDEN_WHEN_STASHED = 0.55f; 154 /** 155 * How long to wait to animate the stack for stashing. 156 */ 157 private static final int ANIMATE_STASH_DELAY = 700; 158 159 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG = 160 new PhysicsAnimator.SpringConfig( 161 StackAnimationController.IME_ANIMATION_STIFFNESS, 162 StackAnimationController.DEFAULT_BOUNCINESS); 163 164 private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = 165 new PhysicsAnimator.SpringConfig(300f, 0.9f); 166 167 private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig = 168 new PhysicsAnimator.SpringConfig(900f, 1f); 169 170 private final PhysicsAnimator.SpringConfig mTranslateSpringConfig = 171 new PhysicsAnimator.SpringConfig( 172 SpringForce.STIFFNESS_VERY_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY); 173 174 /** 175 * Handler to use for all delayed animations - this way, we can easily cancel them before 176 * starting a new animation. 177 */ 178 private final ShellExecutor mMainExecutor; 179 private Runnable mDelayedAnimation; 180 181 /** 182 * Interface to synchronize {@link View} state and the screen. 183 * 184 * {@hide} 185 */ 186 public interface SurfaceSynchronizer { 187 /** 188 * Wait until requested change on a {@link View} is reflected on the screen. 189 * 190 * @param callback callback to run after the change is reflected on the screen. 191 */ syncSurfaceAndRun(Runnable callback)192 void syncSurfaceAndRun(Runnable callback); 193 } 194 195 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER = 196 new SurfaceSynchronizer() { 197 @Override 198 public void syncSurfaceAndRun(Runnable callback) { 199 Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() { 200 // Just wait 2 frames. There is no guarantee, but this is usually enough 201 // time that the requested change is reflected on the screen. 202 // TODO: Once SurfaceFlinger provide APIs to sync the state of 203 // {@code View} and surfaces, rewrite this logic with them. 204 private int mFrameWait = 2; 205 206 @Override 207 public void doFrame(long frameTimeNanos) { 208 if (--mFrameWait > 0) { 209 Choreographer.getInstance().postFrameCallback(this); 210 } else { 211 callback.run(); 212 } 213 } 214 }; 215 Choreographer.getInstance().postFrameCallback(frameCallback); 216 } 217 }; 218 private final BubbleStackViewManager mManager; 219 private final BubbleData mBubbleData; 220 private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider; 221 private StackViewState mStackViewState = new StackViewState(); 222 223 private final ValueAnimator mDismissBubbleAnimator; 224 225 private PhysicsAnimationLayout mBubbleContainer; 226 private StackAnimationController mStackAnimationController; 227 private ExpandedAnimationController mExpandedAnimationController; 228 private ExpandedViewAnimationController mExpandedViewAnimationController; 229 230 private View mScrim; 231 @Nullable 232 private ViewPropertyAnimator mScrimAnimation; 233 private View mManageMenuScrim; 234 private FrameLayout mExpandedViewContainer; 235 236 /** Matrix used to scale the expanded view container with a given pivot point. */ 237 private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix(); 238 239 /** 240 * SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate 241 * between bubble activities without needing both to be alive at the same time. 242 */ 243 private SurfaceView mAnimatingOutSurfaceView; 244 private boolean mAnimatingOutSurfaceReady; 245 246 /** Container for the animating-out SurfaceView. */ 247 private FrameLayout mAnimatingOutSurfaceContainer; 248 249 /** Animator for animating the alpha value of the animating out SurfaceView. */ 250 private final ValueAnimator mAnimatingOutSurfaceAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); 251 252 /** 253 * Buffer containing a screenshot of the animating-out bubble. This is drawn into the 254 * SurfaceView during animations. 255 */ 256 private ScreenCapture.ScreenshotHardwareBuffer mAnimatingOutBubbleBuffer; 257 258 private BubbleFlyoutView mFlyout; 259 /** Runnable that fades out the flyout and then sets it to GONE. */ 260 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */); 261 /** 262 * Callback to run after the flyout hides. Also called if a new flyout is shown before the 263 * previous one animates out. 264 */ 265 private Runnable mAfterFlyoutHidden; 266 /** 267 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout 268 * once it collapses. 269 */ 270 @Nullable 271 private BubbleViewProvider mBubbleToExpandAfterFlyoutCollapse = null; 272 273 /** Layout change listener that moves the stack to the nearest valid position on rotation. */ 274 private OnLayoutChangeListener mOrientationChangedListener; 275 276 @Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation; 277 278 private int mBubbleSize; 279 private int mBubbleElevation; 280 private int mBubbleTouchPadding; 281 private int mExpandedViewPadding; 282 private int mCornerRadius; 283 @Nullable private BubbleViewProvider mExpandedBubble; 284 private boolean mIsExpanded; 285 286 /** Whether the stack is currently on the left side of the screen, or animating there. */ 287 private boolean mStackOnLeftOrWillBe = true; 288 289 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */ 290 private boolean mIsGestureInProgress = false; 291 292 /** Whether or not the stack is temporarily invisible off the side of the screen. */ 293 private boolean mTemporarilyInvisible = false; 294 295 /** Whether we're in the middle of dragging the stack around by touch. */ 296 private boolean mIsDraggingStack = false; 297 298 /** Whether the expanded view has been hidden, because we are dragging out a bubble. */ 299 private boolean mExpandedViewTemporarilyHidden = false; 300 301 /** 302 * Whether the last bubble is being removed when expanded, which impacts the collapse animation. 303 */ 304 private boolean mRemovingLastBubbleWhileExpanded = false; 305 306 /** 307 * Whether sensitive notification protection should disable flyout 308 */ 309 private boolean mSensitiveNotificationProtectionActive = false; 310 311 /** Animator for animating the expanded view's alpha (including the TaskView inside it). */ 312 private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); 313 314 /** 315 * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore 316 * touches from other pointer indices. 317 */ 318 private int mPointerIndexDown = -1; 319 320 /** Indicates whether bubbles should be reordered at the end of a gesture. */ 321 private boolean mShouldReorderBubblesAfterGestureCompletes = false; 322 323 @Nullable 324 private BubblesNavBarGestureTracker mBubblesNavBarGestureTracker; 325 326 /** Description of current animation controller state. */ dump(PrintWriter pw)327 public void dump(PrintWriter pw) { 328 pw.println("Stack view state:"); 329 330 String bubblesOnScreen = BubbleDebugConfig.formatBubblesString( 331 getBubblesOnScreen(), getExpandedBubble()); 332 pw.println(" bubbles on screen: "); pw.println(bubblesOnScreen); 333 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress); 334 pw.print(" showingDismiss: "); pw.println(mDismissView.isShowing()); 335 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating); 336 pw.print(" expandedContainerVis: "); pw.println(mExpandedViewContainer.getVisibility()); 337 pw.print(" expandedContainerAlpha: "); pw.println(mExpandedViewContainer.getAlpha()); 338 pw.print(" expandedContainerMatrix: "); 339 pw.println(mExpandedViewContainer.getAnimationMatrix()); 340 pw.print(" stack visibility : "); pw.println(getVisibility()); 341 pw.print(" temporarilyInvisible: "); pw.println(mTemporarilyInvisible); 342 mStackAnimationController.dump(pw); 343 mExpandedAnimationController.dump(pw); 344 345 if (mExpandedBubble != null) { 346 pw.println("Expanded bubble state:"); 347 pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey()); 348 349 final BubbleExpandedView expandedView = getExpandedView(); 350 351 if (expandedView != null) { 352 pw.println(" expandedViewVis: " + expandedView.getVisibility()); 353 pw.println(" expandedViewAlpha: " + expandedView.getAlpha()); 354 pw.println(" expandedViewTaskId: " + expandedView.getTaskId()); 355 356 final View av = expandedView.getTaskView(); 357 358 if (av != null) { 359 pw.println(" activityViewVis: " + av.getVisibility()); 360 pw.println(" activityViewAlpha: " + av.getAlpha()); 361 } else { 362 pw.println(" activityView is null"); 363 } 364 } else { 365 pw.println("Expanded bubble view state: expanded bubble view is null"); 366 } 367 } else { 368 pw.println("Expanded bubble state: expanded bubble is null"); 369 } 370 } 371 372 private Bubbles.BubbleExpandListener mExpandListener; 373 374 /** Callback to run when we want to unbubble the given notification's conversation. */ 375 private Consumer<String> mUnbubbleConversationCallback; 376 377 private boolean mViewUpdatedRequested = false; 378 private boolean mIsExpansionAnimating = false; 379 private boolean mIsBubbleSwitchAnimating = false; 380 381 /** The view to shrink and apply alpha to when magneted to the dismiss target. */ 382 @Nullable private View mViewBeingDismissed; 383 384 private Rect mTempRect = new Rect(); 385 386 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); 387 388 private ViewTreeObserver.OnPreDrawListener mViewUpdater = 389 new ViewTreeObserver.OnPreDrawListener() { 390 @Override 391 public boolean onPreDraw() { 392 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 393 updateExpandedView(); 394 mViewUpdatedRequested = false; 395 return true; 396 } 397 }; 398 399 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater = 400 this::updateSystemGestureExcludeRects; 401 402 /** Float property that 'drags' the flyout. */ 403 private final FloatPropertyCompat mFlyoutCollapseProperty = 404 new FloatPropertyCompat("FlyoutCollapseSpring") { 405 @Override 406 public float getValue(Object o) { 407 return mFlyoutDragDeltaX; 408 } 409 410 @Override 411 public void setValue(Object o, float v) { 412 setFlyoutStateForDragLength(v); 413 } 414 }; 415 416 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */ 417 private final SpringAnimation mFlyoutTransitionSpring = 418 new SpringAnimation(this, mFlyoutCollapseProperty); 419 420 /** Distance the flyout has been dragged in the X axis. */ 421 private float mFlyoutDragDeltaX = 0f; 422 423 /** 424 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings. 425 */ 426 private Runnable mAnimateInFlyout; 427 428 /** 429 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides 430 * it immediately. 431 */ 432 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring = 433 (dynamicAnimation, b, v, v1) -> { 434 if (mFlyoutDragDeltaX == 0) { 435 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 436 } else { 437 mFlyout.hideFlyout(); 438 } 439 }; 440 441 @NonNull 442 private final SurfaceSynchronizer mSurfaceSynchronizer; 443 444 /** 445 * The currently magnetized object, which is being dragged and will be attracted to the magnetic 446 * dismiss target. 447 * 448 * This is either the stack itself, or an individual bubble. 449 */ 450 private MagnetizedObject<?> mMagnetizedObject; 451 452 /** 453 * The MagneticTarget instance for our circular dismiss view. This is added to the 454 * MagnetizedObject instances for the stack and any dragged-out bubbles. 455 */ 456 private MagnetizedObject.MagneticTarget mMagneticTarget; 457 458 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */ 459 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener = 460 new MagnetizedObject.MagnetListener() { 461 462 @Override 463 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target, 464 @NonNull MagnetizedObject<?> draggedObject) { 465 Object underlyingObject = draggedObject.getUnderlyingObject(); 466 if (underlyingObject instanceof View) { 467 View view = (View) underlyingObject; 468 animateDismissBubble(view, true); 469 } 470 } 471 472 @Override 473 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 474 @NonNull MagnetizedObject<?> draggedObject, 475 float velX, float velY, boolean wasFlungOut) { 476 Object underlyingObject = draggedObject.getUnderlyingObject(); 477 if (underlyingObject instanceof View) { 478 View view = (View) underlyingObject; 479 animateDismissBubble(view, false); 480 481 if (wasFlungOut) { 482 mExpandedAnimationController.snapBubbleBack(view, velX, velY); 483 mDismissView.hide(); 484 } else { 485 mExpandedAnimationController.onUnstuckFromTarget(); 486 } 487 } 488 } 489 490 @Override 491 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, 492 @NonNull MagnetizedObject<?> draggedObject) { 493 Object underlyingObject = draggedObject.getUnderlyingObject(); 494 if (underlyingObject instanceof View) { 495 View view = (View) underlyingObject; 496 mExpandedAnimationController.dismissDraggedOutBubble( 497 view /* bubble */, 498 mDismissView.getHeight() /* translationYBy */, 499 () -> dismissBubbleIfExists( 500 mBubbleData.getBubbleWithView(view)) /* after */); 501 } 502 503 mDismissView.hide(); 504 } 505 }; 506 507 /** Magnet listener that handles animating and dismissing the entire stack. */ 508 private final MagnetizedObject.MagnetListener mStackMagnetListener = 509 new MagnetizedObject.MagnetListener() { 510 @Override 511 public void onStuckToTarget( 512 @NonNull MagnetizedObject.MagneticTarget target, 513 @NonNull MagnetizedObject<?> draggedObject) { 514 animateDismissBubble(mBubbleContainer, true); 515 } 516 517 @Override 518 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 519 @NonNull MagnetizedObject<?> draggedObject, 520 float velX, float velY, boolean wasFlungOut) { 521 animateDismissBubble(mBubbleContainer, false); 522 if (wasFlungOut) { 523 mStackAnimationController.flingStackThenSpringToEdge( 524 mStackAnimationController.getStackPosition().x, velX, velY); 525 mDismissView.hide(); 526 } else { 527 mStackAnimationController.onUnstuckFromTarget(); 528 } 529 } 530 531 @Override 532 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target, 533 @NonNull MagnetizedObject<?> draggedObject) { 534 mStackAnimationController.animateStackDismissal( 535 mDismissView.getHeight() /* translationYBy */, 536 () -> { 537 mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE); 538 resetDismissAnimator(); 539 } /*after */); 540 mDismissView.hide(); 541 } 542 }; 543 544 /** 545 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack. 546 * When expanded, clicking a bubble either expands that bubble, or collapses the stack. 547 */ 548 private OnClickListener mBubbleClickListener = new OnClickListener() { 549 @Override 550 public void onClick(View view) { 551 // If the touch ended in a click, we're no longer dragging. 552 onDraggingEnded(); 553 554 // Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we 555 // shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust 556 // the animations inflight. 557 if (mIsExpansionAnimating || mIsBubbleSwitchAnimating) { 558 return; 559 } 560 561 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view); 562 563 // If the bubble has since left us, ignore the click. 564 if (clickedBubble == null) { 565 return; 566 } 567 568 final boolean clickedBubbleIsCurrentlyExpandedBubble = mExpandedBubble != null 569 && clickedBubble.getKey().equals(mExpandedBubble.getKey()); 570 571 if (isExpanded()) { 572 mExpandedAnimationController.onGestureFinished(); 573 } 574 575 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) { 576 if (clickedBubble != mBubbleData.getSelectedBubble()) { 577 // Select the clicked bubble. 578 mBubbleData.setSelectedBubble(clickedBubble); 579 } else { 580 // If the clicked bubble is the selected bubble (but not the expanded bubble), 581 // that means overflow was previously expanded. Set the selected bubble 582 // internally without going through BubbleData (which would ignore it since it's 583 // already selected). 584 setSelectedBubble(clickedBubble); 585 } 586 } else { 587 // Otherwise, we either tapped the stack (which means we're collapsed 588 // and should expand) or the currently selected bubble (we're expanded 589 // and should collapse). 590 if (!maybeShowStackEdu() && !mShowedUserEducationInTouchListenerActive) { 591 mBubbleData.setExpanded(!mBubbleData.isExpanded()); 592 } 593 mShowedUserEducationInTouchListenerActive = false; 594 } 595 } 596 }; 597 598 /** 599 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when 600 * collapsed), or individual bubbles (when expanded). 601 */ 602 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() { 603 604 @Override 605 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 606 // If we're expanding or collapsing, consume but ignore all touch events. 607 if (mIsExpansionAnimating) { 608 return true; 609 } 610 611 mShowedUserEducationInTouchListenerActive = false; 612 if (maybeShowStackEdu()) { 613 mShowedUserEducationInTouchListenerActive = true; 614 return true; 615 } else if (isStackEduVisible()) { 616 mStackEduView.hide(false /* fromExpansion */); 617 } 618 619 // If the manage menu is visible, just hide it. 620 if (mShowingManage) { 621 showManageMenu(false /* show */); 622 } 623 624 if (mBubbleData.isExpanded()) { 625 if (mManageEduView != null) { 626 mManageEduView.hide(); 627 } 628 629 // If we're expanded, tell the animation controller to prepare to drag this bubble, 630 // dispatching to the individual bubble magnet listener. 631 mExpandedAnimationController.prepareForBubbleDrag( 632 v /* bubble */, 633 mMagneticTarget, 634 mIndividualBubbleMagnetListener); 635 636 hideCurrentInputMethod(); 637 638 // Save the magnetized individual bubble so we can dispatch touch events to it. 639 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut(); 640 } else { 641 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the 642 // animation controller, and hide the flyout. 643 mStackAnimationController.cancelStackPositionAnimations(); 644 mBubbleContainer.setActiveController(mStackAnimationController); 645 hideFlyoutImmediate(); 646 647 // Save the magnetized stack so we can dispatch touch events to it. 648 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(); 649 mMagnetizedObject.clearAllTargets(); 650 mMagnetizedObject.addTarget(mMagneticTarget); 651 mMagnetizedObject.setMagnetListener(mStackMagnetListener); 652 653 mIsDraggingStack = true; 654 655 // Cancel animations to make the stack temporarily invisible, since we're now 656 // dragging it. 657 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 658 } 659 660 passEventToMagnetizedObject(ev); 661 662 // Bubbles are always interested in all touch events! 663 return true; 664 } 665 666 @Override 667 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 668 float viewInitialY, float dx, float dy) { 669 // If we're expanding or collapsing, ignore all touch events. 670 if (mIsExpansionAnimating || mShowedUserEducationInTouchListenerActive) { 671 return; 672 } 673 674 // Show the dismiss target, if we haven't already. 675 mDismissView.show(); 676 677 if (mIsExpanded && mExpandedBubble != null && v.equals(mExpandedBubble.getIconView())) { 678 // Hide the expanded view if we're dragging out the expanded bubble, and we haven't 679 // already hidden it. 680 hideExpandedViewIfNeeded(); 681 } 682 683 // First, see if the magnetized object consumes the event - if so, we shouldn't move the 684 // bubble since it's stuck to the target. 685 if (!passEventToMagnetizedObject(ev)) { 686 updateBubbleShadows(true /* isExpanded */); 687 if (mBubbleData.isExpanded()) { 688 mExpandedAnimationController.dragBubbleOut( 689 v, viewInitialX + dx, viewInitialY + dy); 690 } else { 691 if (isStackEduVisible()) { 692 mStackEduView.hide(false /* fromExpansion */); 693 } 694 mStackAnimationController.moveStackFromTouch( 695 viewInitialX + dx, viewInitialY + dy); 696 } 697 } 698 } 699 700 @Override 701 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 702 float viewInitialY, float dx, float dy, float velX, float velY) { 703 // If we're expanding or collapsing, ignore all touch events. 704 if (mIsExpansionAnimating) { 705 return; 706 } 707 if (mShowedUserEducationInTouchListenerActive) { 708 mShowedUserEducationInTouchListenerActive = false; 709 return; 710 } 711 712 // First, see if the magnetized object consumes the event - if so, the bubble was 713 // released in the target or flung out of it, and we should ignore the event. 714 if (!passEventToMagnetizedObject(ev)) { 715 if (mBubbleData.isExpanded()) { 716 mExpandedAnimationController.snapBubbleBack(v, velX, velY); 717 718 // Re-show the expanded view if we hid it. 719 showExpandedViewIfNeeded(); 720 } else { 721 // Fling the stack to the edge, and save whether or not it's going to end up on 722 // the left side of the screen. 723 final boolean oldOnLeft = mStackOnLeftOrWillBe; 724 mStackOnLeftOrWillBe = 725 mStackAnimationController.flingStackThenSpringToEdge( 726 viewInitialX + dx, velX, velY) <= 0; 727 final boolean updateForCollapsedStack = oldOnLeft != mStackOnLeftOrWillBe; 728 updateBadges(updateForCollapsedStack); 729 logBubbleEvent(null /* no bubble associated with bubble stack move */, 730 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); 731 } 732 mDismissView.hide(); 733 } 734 735 onDraggingEnded(); 736 737 // Hide the stack after a delay, if needed. 738 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 739 animateStashedState(false /* stashImmediately */); 740 } 741 742 @Override 743 public void onCancel(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 744 float viewInitialY) { 745 animateStashedState(false /* stashImmediately */); 746 } 747 }; 748 749 /** Touch listener set on the whole view that forwards event to the swipe up listener. */ 750 private final RelativeTouchListener mContainerSwipeListener = new RelativeTouchListener() { 751 @Override 752 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 753 // Pass move event on to swipe listener 754 mSwipeUpListener.onDown(ev.getX(), ev.getY()); 755 return true; 756 } 757 758 @Override 759 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 760 float viewInitialY, float dx, float dy) { 761 // Pass move event on to swipe listener 762 mSwipeUpListener.onMove(dx, dy); 763 } 764 765 @Override 766 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 767 float viewInitialY, float dx, float dy, float velX, float velY) { 768 // Pass up even on to swipe listener 769 mSwipeUpListener.onUp(velX, velY); 770 } 771 }; 772 773 /** MotionEventListener that listens from home gesture swipe event. */ 774 private final MotionEventListener mSwipeUpListener = new MotionEventListener() { 775 @Override 776 public void onDown(float x, float y) {} 777 778 @Override 779 public void onMove(float dx, float dy) { 780 if (isManageEduVisible() || isStackEduVisible()) { 781 return; 782 } 783 784 if (mShowingManage) { 785 showManageMenu(false /* show */); 786 } 787 // Only allow up, normalize for up direction 788 float collapsed = -Math.min(dy, 0); 789 mExpandedViewAnimationController.updateDrag((int) collapsed); 790 791 // Update scrim if it's not animating already 792 if (mScrimAnimation == null) { 793 mScrim.setAlpha(getScrimAlphaForDrag(collapsed)); 794 } 795 } 796 797 @Override 798 public void onCancel() { 799 mExpandedViewAnimationController.animateBackToExpanded(); 800 } 801 802 @Override 803 public void onUp(float velX, float velY) { 804 mExpandedViewAnimationController.setSwipeVelocity(velY); 805 if (mExpandedViewAnimationController.shouldCollapse()) { 806 // Update data first and start the animation when we are processing change 807 mBubbleData.setExpanded(false); 808 } else { 809 mExpandedViewAnimationController.animateBackToExpanded(); 810 811 // Update scrim if it's not animating already 812 if (mScrimAnimation == null) { 813 showScrim(true, null /* runnable */); 814 } 815 } 816 } 817 818 private float getScrimAlphaForDrag(float dragAmount) { 819 // dragAmount should be negative as we allow scroll up only 820 BubbleExpandedView expandedView = getExpandedView(); 821 if (expandedView != null) { 822 float alphaRange = BUBBLE_EXPANDED_SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG; 823 824 int dragMax = expandedView.getContentHeight(); 825 float dragFraction = dragAmount / dragMax; 826 827 return Math.max(BUBBLE_EXPANDED_SCRIM_ALPHA - alphaRange * dragFraction, 828 MIN_SCRIM_ALPHA_FOR_DRAG); 829 } 830 return BUBBLE_EXPANDED_SCRIM_ALPHA; 831 } 832 }; 833 834 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */ 835 private OnClickListener mFlyoutClickListener = new OnClickListener() { 836 @Override 837 public void onClick(View view) { 838 if (maybeShowStackEdu()) { 839 // If we're showing user education, don't open the bubble show the education first 840 mBubbleToExpandAfterFlyoutCollapse = null; 841 } else { 842 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble(); 843 } 844 845 mFlyout.removeCallbacks(mHideFlyout); 846 mHideFlyout.run(); 847 } 848 }; 849 850 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */ 851 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() { 852 853 @Override 854 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) { 855 mFlyout.removeCallbacks(mHideFlyout); 856 return true; 857 } 858 859 @Override 860 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 861 float viewInitialY, float dx, float dy) { 862 setFlyoutStateForDragLength(dx); 863 } 864 865 @Override 866 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX, 867 float viewInitialY, float dx, float dy, float velX, float velY) { 868 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 869 final boolean metRequiredVelocity = 870 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY; 871 final boolean metRequiredDeltaX = 872 onLeft 873 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS 874 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS; 875 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0; 876 final boolean shouldDismiss = metRequiredVelocity 877 || (metRequiredDeltaX && !isCancelFling); 878 879 mFlyout.removeCallbacks(mHideFlyout); 880 animateFlyoutCollapsed(shouldDismiss, velX); 881 882 maybeShowStackEdu(); 883 } 884 }; 885 886 private boolean mShowingOverflow; 887 private BubbleOverflow mBubbleOverflow; 888 private StackEducationView mStackEduView; 889 private StackEducationView.Manager mStackEducationViewManager; 890 private ManageEducationView mManageEduView; 891 private DismissView mDismissView; 892 893 private ViewGroup mManageMenu; 894 private ViewGroup mManageDontBubbleView; 895 private ViewGroup mManageSettingsView; 896 private ImageView mManageSettingsIcon; 897 private TextView mManageSettingsText; 898 private boolean mShowingManage = false; 899 private boolean mShowedUserEducationInTouchListenerActive = false; 900 private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( 901 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 902 private BubblePositioner mPositioner; 903 904 @SuppressLint("ClickableViewAccessibility") BubbleStackView(Context context, BubbleStackViewManager bubbleStackViewManager, BubblePositioner bubblePositioner, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, Bubbles.SysuiProxy.Provider sysuiProxyProvider, ShellExecutor mainExecutor)905 public BubbleStackView(Context context, BubbleStackViewManager bubbleStackViewManager, 906 BubblePositioner bubblePositioner, BubbleData data, 907 @Nullable SurfaceSynchronizer synchronizer, 908 FloatingContentCoordinator floatingContentCoordinator, 909 Bubbles.SysuiProxy.Provider sysuiProxyProvider, 910 ShellExecutor mainExecutor) { 911 super(context); 912 913 mMainExecutor = mainExecutor; 914 mManager = bubbleStackViewManager; 915 mPositioner = bubblePositioner; 916 mBubbleData = data; 917 mSysuiProxyProvider = sysuiProxyProvider; 918 919 Resources res = getResources(); 920 mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); 921 mBubbleElevation = mPositioner.getBubbleElevation(); 922 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); 923 924 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 925 926 927 final TypedArray ta = mContext.obtainStyledAttributes( 928 new int[]{android.R.attr.dialogCornerRadius}); 929 mCornerRadius = ta.getDimensionPixelSize(0, 0); 930 ta.recycle(); 931 932 final Runnable onBubbleAnimatedOut = () -> { 933 if (getBubbleCount() == 0) { 934 mExpandedViewTemporarilyHidden = false; 935 mManager.onAllBubblesAnimatedOut(); 936 } 937 }; 938 mStackAnimationController = new StackAnimationController( 939 floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, 940 this::animateShadows /* onStackAnimationFinished */, mPositioner); 941 942 mExpandedAnimationController = new ExpandedAnimationController(mPositioner, 943 onBubbleAnimatedOut, this); 944 945 mExpandedViewAnimationController = 946 new ExpandedViewAnimationControllerImpl(context, mPositioner); 947 948 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; 949 950 // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or 951 // is centered. It greatly simplifies translation positioning/animations. Views that will 952 // actually lay out differently in RTL, such as the flyout and expanded view, will set their 953 // layout direction to LOCALE. 954 setLayoutDirection(LAYOUT_DIRECTION_LTR); 955 956 mBubbleContainer = new PhysicsAnimationLayout(context); 957 mBubbleContainer.setActiveController(mStackAnimationController); 958 mBubbleContainer.setElevation(mBubbleElevation); 959 mBubbleContainer.setClipChildren(false); 960 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); 961 962 mExpandedViewContainer = new FrameLayout(context); 963 mExpandedViewContainer.setElevation(mBubbleElevation); 964 mExpandedViewContainer.setClipChildren(false); 965 addView(mExpandedViewContainer); 966 967 mAnimatingOutSurfaceContainer = new FrameLayout(getContext()); 968 mAnimatingOutSurfaceContainer.setLayoutParams( 969 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 970 addView(mAnimatingOutSurfaceContainer); 971 972 mAnimatingOutSurfaceView = new SurfaceView(getContext()); 973 mAnimatingOutSurfaceView.setZOrderOnTop(true); 974 boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( 975 mContext.getResources()); 976 mAnimatingOutSurfaceView.setCornerRadius(supportsRoundedCorners ? mCornerRadius : 0); 977 mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); 978 mAnimatingOutSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 979 @Override 980 public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {} 981 982 @Override 983 public void surfaceCreated(SurfaceHolder surfaceHolder) { 984 mAnimatingOutSurfaceReady = true; 985 } 986 987 @Override 988 public void surfaceDestroyed(SurfaceHolder surfaceHolder) { 989 mAnimatingOutSurfaceReady = false; 990 } 991 }); 992 mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView); 993 994 mAnimatingOutSurfaceContainer.setPadding( 995 mExpandedViewContainer.getPaddingLeft(), 996 mExpandedViewContainer.getPaddingTop(), 997 mExpandedViewContainer.getPaddingRight(), 998 mExpandedViewContainer.getPaddingBottom()); 999 1000 setUpManageMenu(); 1001 1002 setUpFlyout(); 1003 mFlyoutTransitionSpring.setSpring(new SpringForce() 1004 .setStiffness(SpringForce.STIFFNESS_LOW) 1005 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); 1006 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring); 1007 1008 setUpDismissView(); 1009 1010 setClipChildren(false); 1011 setFocusable(true); 1012 mBubbleContainer.bringToFront(); 1013 1014 mBubbleOverflow = mBubbleData.getOverflow(); 1015 1016 if (Flags.enableOptionalBubbleOverflow()) { 1017 showOverflow(mBubbleData.hasOverflowBubbles()); 1018 } else { 1019 mShowingOverflow = true; // if the flags not on this is always true 1020 setUpOverflow(); 1021 } 1022 mScrim = new View(getContext()); 1023 mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1024 mScrim.setBackgroundDrawable(new ColorDrawable( 1025 getResources().getColor(android.R.color.system_neutral1_1000))); 1026 addView(mScrim); 1027 mScrim.setAlpha(0f); 1028 1029 mManageMenuScrim = new View(getContext()); 1030 mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1031 mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( 1032 getResources().getColor(android.R.color.system_neutral1_1000))); 1033 addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 1034 mManageMenuScrim.setAlpha(0f); 1035 mManageMenuScrim.setVisibility(INVISIBLE); 1036 1037 mOrientationChangedListener = 1038 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 1039 mPositioner.update(DeviceConfig.create(mContext, mContext.getSystemService( 1040 WindowManager.class))); 1041 onDisplaySizeChanged(); 1042 mExpandedAnimationController.updateResources(); 1043 mExpandedAnimationController.onOrientationChanged(); 1044 mStackAnimationController.updateResources(); 1045 mBubbleOverflow.updateResources(); 1046 1047 if (!isStackEduVisible() && mRelativeStackPositionBeforeRotation != null) { 1048 mStackAnimationController.setStackPosition( 1049 mRelativeStackPositionBeforeRotation); 1050 mRelativeStackPositionBeforeRotation = null; 1051 } 1052 1053 if (mIsExpanded) { 1054 // update the expanded view and pointer location for the new orientation. 1055 hideFlyoutImmediate(); 1056 mExpandedViewContainer.setAlpha(0f); 1057 updateExpandedView(); 1058 updateOverflowVisibility(); 1059 updatePointerPosition(false); 1060 requestUpdate(); 1061 if (mShowingManage) { 1062 // if we're showing the menu after rotation, post it to the looper 1063 // to make sure that the location of the menu button is correct 1064 post(() -> showManageMenu(true)); 1065 } else { 1066 showManageMenu(false); 1067 } 1068 1069 PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), 1070 getState()); 1071 final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, 1072 mPositioner.showBubblesVertically() ? p.y : p.x); 1073 mExpandedViewContainer.setTranslationX(0f); 1074 mExpandedViewContainer.setTranslationY(translationY); 1075 mExpandedViewContainer.setAlpha(1f); 1076 } 1077 1078 removeOnLayoutChangeListener(mOrientationChangedListener); 1079 }; 1080 final float maxDismissSize = getResources().getDimensionPixelSize( 1081 R.dimen.dismiss_circle_size); 1082 final float minDismissSize = getResources().getDimensionPixelSize( 1083 R.dimen.dismiss_circle_small); 1084 final float sizePercent = minDismissSize / maxDismissSize; 1085 mDismissBubbleAnimator = ValueAnimator.ofFloat(1f, 0f); 1086 mDismissBubbleAnimator.addUpdateListener(animation -> { 1087 final float animatedValue = (float) animation.getAnimatedValue(); 1088 if (mDismissView != null) { 1089 mDismissView.setPivotX((mDismissView.getRight() - mDismissView.getLeft()) / 2f); 1090 mDismissView.setPivotY((mDismissView.getBottom() - mDismissView.getTop()) / 2f); 1091 final float scaleValue = Math.max(animatedValue, sizePercent); 1092 mDismissView.getCircle().setScaleX(scaleValue); 1093 mDismissView.getCircle().setScaleY(scaleValue); 1094 } 1095 if (mViewBeingDismissed != null) { 1096 mViewBeingDismissed.setAlpha(Math.max(animatedValue, 0.7f)); 1097 } 1098 }); 1099 1100 // If the stack itself is clicked, it means none of its touchable views (bubbles, flyouts, 1101 // TaskView, etc.) were touched. Collapse the stack if it's expanded. 1102 setOnClickListener(view -> { 1103 if (mShowingManage) { 1104 showManageMenu(false /* show */); 1105 } else if (isManageEduVisible()) { 1106 mManageEduView.hide(); 1107 } else if (isStackEduVisible()) { 1108 mStackEduView.hide(false /* isExpanding */); 1109 } else if (mBubbleData.isExpanded()) { 1110 mBubbleData.setExpanded(false); 1111 } else { 1112 maybeShowStackEdu(); 1113 } 1114 onDraggingEnded(); 1115 }); 1116 1117 animate() 1118 .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED) 1119 .setDuration(FADE_IN_DURATION); 1120 1121 mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); 1122 mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); 1123 mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() { 1124 @Override 1125 public void onAnimationStart(Animator animation) { 1126 BubbleExpandedView expandedView = getExpandedView(); 1127 if (expandedView != null) { 1128 // We need to be Z ordered on top in order for alpha animations to work. 1129 expandedView.setSurfaceZOrderedOnTop(true); 1130 expandedView.setAnimating(true); 1131 mExpandedViewContainer.setVisibility(VISIBLE); 1132 } 1133 } 1134 1135 @Override 1136 public void onAnimationEnd(Animator animation) { 1137 BubbleExpandedView expandedView = getExpandedView(); 1138 if (expandedView != null 1139 // The surface needs to be Z ordered on top for alpha values to work on the 1140 // TaskView, and if we're temporarily hidden, we are still on the screen 1141 // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha 1142 // = 0f remains in effect. 1143 && !mExpandedViewTemporarilyHidden) { 1144 expandedView.setSurfaceZOrderedOnTop(false); 1145 expandedView.setAnimating(false); 1146 } 1147 } 1148 }); 1149 mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { 1150 BubbleExpandedView expandedView = getExpandedView(); 1151 if (expandedView != null) { 1152 float alpha = (float) valueAnimator.getAnimatedValue(); 1153 expandedView.setContentAlpha(alpha); 1154 expandedView.setBackgroundAlpha(alpha); 1155 } 1156 }); 1157 1158 mAnimatingOutSurfaceAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); 1159 mAnimatingOutSurfaceAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); 1160 mAnimatingOutSurfaceAlphaAnimator.addUpdateListener(valueAnimator -> { 1161 if (!mExpandedViewTemporarilyHidden) { 1162 mAnimatingOutSurfaceView.setAlpha((float) valueAnimator.getAnimatedValue()); 1163 } 1164 }); 1165 mAnimatingOutSurfaceAlphaAnimator.addListener(new AnimatorListenerAdapter() { 1166 @Override 1167 public void onAnimationEnd(Animator animation) { 1168 releaseAnimatingOutBubbleBuffer(); 1169 } 1170 }); 1171 } 1172 1173 /** 1174 * Reset state related to dragging. 1175 */ onDraggingEnded()1176 private void onDraggingEnded() { 1177 mIsDraggingStack = false; 1178 mMagnetizedObject = null; 1179 } 1180 1181 /** 1182 * Sets whether or not the stack should become temporarily invisible by moving off the side of 1183 * the screen. 1184 * 1185 * If a flyout comes in while it's invisible, it will animate back in while the flyout is 1186 * showing but disappear again when the flyout is gone. 1187 */ setTemporarilyInvisible(boolean invisible)1188 public void setTemporarilyInvisible(boolean invisible) { 1189 mTemporarilyInvisible = invisible; 1190 1191 // If we are animating out, hide immediately if possible so we animate out with the status 1192 // bar. 1193 updateTemporarilyInvisibleAnimation(invisible /* hideImmediately */); 1194 } 1195 1196 /** 1197 * Animates the stack to be temporarily invisible, if needed. 1198 * 1199 * If we're currently dragging the stack, or a flyout is visible, the stack will remain visible. 1200 * regardless of the value of {@link #mTemporarilyInvisible}. This method is called on ACTION_UP 1201 * as well as whenever a flyout hides, so we will animate invisible at that point if needed. 1202 */ updateTemporarilyInvisibleAnimation(boolean hideImmediately)1203 private void updateTemporarilyInvisibleAnimation(boolean hideImmediately) { 1204 removeCallbacks(mAnimateTemporarilyInvisibleImmediate); 1205 1206 if (mIsDraggingStack) { 1207 // If we're dragging the stack, don't animate it invisible. 1208 return; 1209 } 1210 1211 final boolean shouldHide = 1212 mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE; 1213 1214 postDelayed(mAnimateTemporarilyInvisibleImmediate, 1215 shouldHide && !hideImmediately ? ANIMATE_TEMPORARILY_INVISIBLE_DELAY : 0); 1216 } 1217 1218 private final Runnable mAnimateTemporarilyInvisibleImmediate = () -> { 1219 if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) { 1220 // To calculate a distance, bubble stack needs to be moved to become hidden, 1221 // we need to take into account that the bubble stack is positioned on the edge 1222 // of the available screen rect, which can be offset by system bars and cutouts. 1223 if (mStackAnimationController.isStackOnLeftSide()) { 1224 int availableRectOffsetX = 1225 mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left; 1226 mBubbleContainer 1227 .animate() 1228 .translationX(-(mBubbleSize + availableRectOffsetX)) 1229 .start(); 1230 } else { 1231 int availableRectOffsetX = 1232 mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right; 1233 mBubbleContainer.animate().translationX(mBubbleSize - availableRectOffsetX).start(); 1234 } 1235 } else { 1236 mBubbleContainer.animate().translationX(0).start(); 1237 } 1238 }; 1239 1240 /** 1241 * Animates the bubble stack to stash along the edge of the screen. 1242 * 1243 * @param stashImmediately whether the stash should happen immediately or without delay. 1244 */ animateStashedState(boolean stashImmediately)1245 private void animateStashedState(boolean stashImmediately) { 1246 if (!Flags.enableBubbleStashing()) return; 1247 1248 removeCallbacks(mAnimateStashedState); 1249 1250 postDelayed(mAnimateStashedState, stashImmediately ? 0 : ANIMATE_STASH_DELAY); 1251 } 1252 1253 private final Runnable mAnimateStashedState = () -> { 1254 if (mFlyout.getVisibility() != View.VISIBLE 1255 && !mIsDraggingStack 1256 && !isExpansionAnimating() 1257 && !isExpanded() 1258 && !isStackEduVisible()) { 1259 // To calculate a distance, bubble stack needs to be moved to become stashed, 1260 // we need to take into account that the bubble stack is positioned on the edge 1261 // of the available screen rect, which can be offset by system bars and cutouts. 1262 final float amountOffscreen = mBubbleSize - (mBubbleSize * PERCENT_HIDDEN_WHEN_STASHED); 1263 if (mStackAnimationController.isStackOnLeftSide()) { 1264 int availableRectOffsetX = 1265 mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left; 1266 mBubbleContainer 1267 .animate() 1268 .translationX(-(amountOffscreen + availableRectOffsetX)) 1269 .start(); 1270 } else { 1271 int availableRectOffsetX = 1272 mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right; 1273 mBubbleContainer.animate() 1274 .translationX(amountOffscreen - availableRectOffsetX) 1275 .start(); 1276 } 1277 } 1278 }; 1279 setUpOverflow()1280 private void setUpOverflow() { 1281 resetOverflowView(); 1282 mBubbleContainer.addView(mBubbleOverflow.getIconView(), 1283 mBubbleContainer.getChildCount() /* index */, 1284 new FrameLayout.LayoutParams(mBubbleSize, mBubbleSize)); 1285 updateOverflow(); 1286 mBubbleOverflow.getIconView().setOnClickListener((View v) -> { 1287 mBubbleData.setShowingOverflow(true); 1288 mBubbleData.setSelectedBubble(mBubbleOverflow); 1289 mBubbleData.setExpanded(true); 1290 }); 1291 } 1292 setUpDismissView()1293 private void setUpDismissView() { 1294 if (mDismissView != null) { 1295 removeView(mDismissView); 1296 } 1297 mDismissView = new DismissView(getContext()); 1298 DismissViewUtils.setup(mDismissView); 1299 int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation); 1300 1301 addView(mDismissView); 1302 mDismissView.setElevation(elevation); 1303 1304 final ContentResolver contentResolver = getContext().getContentResolver(); 1305 final int dismissRadius = Settings.Secure.getInt( 1306 contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */); 1307 1308 // Save the MagneticTarget instance for the newly set up view - we'll add this to the 1309 // MagnetizedObjects when the dismiss view gets shown. 1310 mMagneticTarget = new MagnetizedObject.MagneticTarget( 1311 mDismissView.getCircle(), dismissRadius); 1312 mBubbleContainer.bringToFront(); 1313 } 1314 1315 // TODO: Create ManageMenuView and move setup / animations there setUpManageMenu()1316 private void setUpManageMenu() { 1317 if (mManageMenu != null) { 1318 removeView(mManageMenu); 1319 } 1320 1321 mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( 1322 R.layout.bubble_manage_menu, this, false); 1323 mManageMenu.setVisibility(View.INVISIBLE); 1324 1325 final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ 1326 com.android.internal.R.attr.materialColorSurfaceBright}); 1327 final int menuBackgroundColor = ta.getColor(0, Color.WHITE); 1328 ta.recycle(); 1329 mManageMenu.getBackground().setColorFilter(menuBackgroundColor, PorterDuff.Mode.SRC_IN); 1330 1331 PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); 1332 1333 mManageMenu.setOutlineProvider(new ViewOutlineProvider() { 1334 @Override 1335 public void getOutline(View view, Outline outline) { 1336 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 1337 } 1338 }); 1339 mManageMenu.setClipToOutline(true); 1340 1341 mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( 1342 view -> { 1343 showManageMenu(false /* show */); 1344 dismissBubbleIfExists(mBubbleData.getSelectedBubble()); 1345 }); 1346 1347 mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( 1348 view -> { 1349 showManageMenu(false /* show */); 1350 mUnbubbleConversationCallback.accept(mBubbleData.getSelectedBubble().getKey()); 1351 }); 1352 1353 mManageDontBubbleView = mManageMenu 1354 .findViewById(R.id.bubble_manage_menu_dont_bubble_container); 1355 1356 mManageSettingsView = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container); 1357 mManageSettingsView.setOnClickListener( 1358 view -> { 1359 showManageMenu(false /* show */); 1360 final BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); 1361 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 1362 // If it's in the stack it's a proper Bubble. 1363 final Intent intent = ((Bubble) bubble).getSettingsIntent(mContext); 1364 mBubbleData.setExpanded(false); 1365 mContext.startActivityAsUser(intent, ((Bubble) bubble).getUser()); 1366 logBubbleEvent(bubble, 1367 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); 1368 } 1369 }); 1370 1371 mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); 1372 mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); 1373 1374 // The menu itself should respect locale direction so the icons are on the correct side. 1375 mManageMenu.setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 1376 addView(mManageMenu); 1377 updateManageButtonListener(); 1378 } 1379 1380 /** 1381 * Whether the selected bubble is conversation bubble 1382 */ isConversationBubble()1383 private boolean isConversationBubble() { 1384 BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); 1385 return bubble instanceof Bubble && ((Bubble) bubble).isConversation(); 1386 } 1387 1388 /** 1389 * Whether the educational view should show for the expanded view "manage" menu. 1390 */ shouldShowManageEdu()1391 private boolean shouldShowManageEdu() { 1392 if (!isConversationBubble()) { 1393 // We only show user education for conversation bubbles right now 1394 return false; 1395 } 1396 final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION); 1397 final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) 1398 && getExpandedView() != null; 1399 ProtoLog.d(WM_SHELL_BUBBLES, "Show manage edu=%b", shouldShow); 1400 if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) { 1401 Log.w(TAG, "Want to show manage edu, but it is forced hidden"); 1402 return false; 1403 } 1404 return shouldShow; 1405 } 1406 1407 /** 1408 * Show manage education if should show and was not showing before. 1409 */ maybeShowManageEdu()1410 private void maybeShowManageEdu() { 1411 if (!shouldShowManageEdu()) { 1412 return; 1413 } 1414 if (mManageEduView == null) { 1415 mManageEduView = new ManageEducationView(mContext, mPositioner); 1416 addView(mManageEduView); 1417 } 1418 showManageEdu(); 1419 } 1420 1421 /** 1422 * Show manage education if was not showing before. 1423 */ showManageEdu()1424 private void showManageEdu() { 1425 BubbleExpandedView expandedView = getExpandedView(); 1426 if (expandedView == null) return; 1427 mManageEduView.show(expandedView, mStackAnimationController.isStackOnLeftSide()); 1428 } 1429 1430 @VisibleForTesting isManageEduVisible()1431 public boolean isManageEduVisible() { 1432 return mManageEduView != null && mManageEduView.getVisibility() == VISIBLE; 1433 } 1434 1435 /** 1436 * Whether education view should show for the collapsed stack. 1437 */ shouldShowStackEdu()1438 private boolean shouldShowStackEdu() { 1439 if (!isConversationBubble()) { 1440 // We only show user education for conversation bubbles right now 1441 return false; 1442 } 1443 final boolean seen = getPrefBoolean(StackEducationView.PREF_STACK_EDUCATION); 1444 final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); 1445 ProtoLog.d(WM_SHELL_BUBBLES, "Show stack edu=%b", shouldShow); 1446 if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) { 1447 Log.w(TAG, "Want to show stack edu, but it is forced hidden"); 1448 return false; 1449 } 1450 return shouldShow; 1451 } 1452 getPrefBoolean(String key)1453 private boolean getPrefBoolean(String key) { 1454 return mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE) 1455 .getBoolean(key, false /* default */); 1456 } 1457 1458 /** 1459 * @return true if education view for collapsed stack should show and was not showing before. 1460 */ maybeShowStackEdu()1461 private boolean maybeShowStackEdu() { 1462 if (!shouldShowStackEdu() || isExpanded()) { 1463 return false; 1464 } 1465 if (mStackEduView == null) { 1466 mStackEducationViewManager = mManager::updateWindowFlagsForBackpress; 1467 mStackEduView = 1468 new StackEducationView(mContext, mPositioner, mStackEducationViewManager); 1469 addView(mStackEduView); 1470 } 1471 return showStackEdu(); 1472 } 1473 1474 /** 1475 * @return true if education view for the collapsed stack was not showing before. 1476 */ showStackEdu()1477 private boolean showStackEdu() { 1478 // Stack appears on top of the education views 1479 mBubbleContainer.bringToFront(); 1480 // Ensure the stack is in the correct spot 1481 PointF position = mPositioner.getStartPosition( 1482 mStackAnimationController.isStackOnLeftSide() ? LEFT : RIGHT); 1483 // Animate stack to the position 1484 mStackAnimationController.springStackAfterFling(position.x, position.y); 1485 return mStackEduView.show(position); 1486 } 1487 1488 @VisibleForTesting isStackEduVisible()1489 public boolean isStackEduVisible() { 1490 return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE; 1491 } 1492 1493 // Recreates & shows the education views. Call when a theme/config change happens. updateUserEdu()1494 private void updateUserEdu() { 1495 if (isStackEduVisible() && !mStackEduView.isHiding()) { 1496 removeView(mStackEduView); 1497 mStackEducationViewManager = mManager::updateWindowFlagsForBackpress; 1498 mStackEduView = 1499 new StackEducationView(mContext, mPositioner, mStackEducationViewManager); 1500 addView(mStackEduView); 1501 showStackEdu(); 1502 } 1503 if (isManageEduVisible()) { 1504 removeView(mManageEduView); 1505 mManageEduView = new ManageEducationView(mContext, mPositioner); 1506 addView(mManageEduView); 1507 showManageEdu(); 1508 } 1509 } 1510 1511 @SuppressLint("ClickableViewAccessibility") setUpFlyout()1512 private void setUpFlyout() { 1513 if (mFlyout != null) { 1514 removeView(mFlyout); 1515 } 1516 mFlyout = new BubbleFlyoutView(getContext(), mPositioner); 1517 mFlyout.setVisibility(GONE); 1518 mFlyout.setOnClickListener(mFlyoutClickListener); 1519 mFlyout.setOnTouchListener(mFlyoutTouchListener); 1520 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 1521 } 1522 updateFontScale()1523 void updateFontScale() { 1524 setUpManageMenu(); 1525 mFlyout.updateFontSize(); 1526 for (Bubble b : mBubbleData.getBubbles()) { 1527 if (b.getExpandedView() != null) { 1528 b.getExpandedView().updateFontSize(); 1529 } 1530 } 1531 if (mShowingOverflow && mBubbleOverflow != null 1532 && mBubbleOverflow.getExpandedView() != null) { 1533 mBubbleOverflow.getExpandedView().updateFontSize(); 1534 } 1535 } 1536 updateLocale()1537 void updateLocale() { 1538 if (mShowingOverflow && mBubbleOverflow != null 1539 && mBubbleOverflow.getExpandedView() != null) { 1540 mBubbleOverflow.getExpandedView().updateLocale(); 1541 } 1542 } 1543 updateOverflow()1544 private void updateOverflow() { 1545 mBubbleOverflow.update(); 1546 if (mShowingOverflow) { 1547 mBubbleContainer.reorderView(mBubbleOverflow.getIconView(), 1548 mBubbleContainer.getChildCount() - 1 /* index */); 1549 } 1550 updateOverflowVisibility(); 1551 } 1552 updateOverflowVisibility()1553 private void updateOverflowVisibility() { 1554 int visibility = GONE; 1555 if (mShowingOverflow) { 1556 if (mIsExpanded || mBubbleData.isShowingOverflow()) { 1557 visibility = VISIBLE; 1558 } 1559 } 1560 if (Flags.enableRetrievableBubbles()) { 1561 if (BubbleOverflow.KEY.equals(mBubbleData.getSelectedBubbleKey()) 1562 && !mBubbleData.hasBubbles()) { 1563 // Hide overflow bubble icon if it is the only bubble 1564 visibility = GONE; 1565 } 1566 } 1567 mBubbleOverflow.setVisible(visibility); 1568 } 1569 updateOverflowDotVisibility(boolean expanding)1570 private void updateOverflowDotVisibility(boolean expanding) { 1571 if (mShowingOverflow && mBubbleOverflow.showDot()) { 1572 mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> { 1573 mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE); 1574 }); 1575 } 1576 } 1577 1578 /** Sets whether the overflow should be visible or not. */ showOverflow(boolean showOverflow)1579 public void showOverflow(boolean showOverflow) { 1580 if (!Flags.enableOptionalBubbleOverflow()) return; 1581 if (mShowingOverflow != showOverflow) { 1582 mShowingOverflow = showOverflow; 1583 if (showOverflow) { 1584 setUpOverflow(); 1585 } else if (mBubbleOverflow != null) { 1586 resetOverflowView(); 1587 } 1588 } 1589 } 1590 1591 /** 1592 * Handle theme changes. 1593 */ onThemeChanged()1594 public void onThemeChanged() { 1595 setUpFlyout(); 1596 setUpManageMenu(); 1597 setUpDismissView(); 1598 updateOverflow(); 1599 updateUserEdu(); 1600 updateExpandedViewTheme(); 1601 mScrim.setBackgroundDrawable(new ColorDrawable( 1602 getResources().getColor(android.R.color.system_neutral1_1000))); 1603 mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( 1604 getResources().getColor(android.R.color.system_neutral1_1000))); 1605 } 1606 1607 /** 1608 * Respond to the phone being rotated by repositioning the stack and hiding any flyouts. 1609 * This is called prior to the rotation occurring, any values that should be updated 1610 * based on the new rotation should occur in {@link #mOrientationChangedListener}. 1611 */ onOrientationChanged()1612 public void onOrientationChanged() { 1613 mRelativeStackPositionBeforeRotation = new RelativeStackPosition( 1614 mPositioner.getRestingPosition(), 1615 mPositioner.getAllowableStackPositionRegion(getBubbleCount())); 1616 addOnLayoutChangeListener(mOrientationChangedListener); 1617 hideFlyoutImmediate(); 1618 } 1619 1620 /** Tells the views with locale-dependent layout direction to resolve the new direction. */ onLayoutDirectionChanged(int direction)1621 public void onLayoutDirectionChanged(int direction) { 1622 mManageMenu.setLayoutDirection(direction); 1623 mFlyout.setLayoutDirection(direction); 1624 if (mStackEduView != null) { 1625 mStackEduView.setLayoutDirection(direction); 1626 } 1627 if (mManageEduView != null) { 1628 mManageEduView.setLayoutDirection(direction); 1629 } 1630 updateExpandedViewDirection(direction); 1631 } 1632 1633 /** Respond to the display size change by recalculating view size and location. */ onDisplaySizeChanged()1634 public void onDisplaySizeChanged() { 1635 updateOverflow(); 1636 setUpFlyout(); 1637 setUpDismissView(); 1638 updateUserEdu(); 1639 mBubbleSize = mPositioner.getBubbleSize(); 1640 for (Bubble b : mBubbleData.getBubbles()) { 1641 if (b.getIconView() == null) { 1642 Log.w(TAG, "Display size changed. Icon null: " + b); 1643 continue; 1644 } 1645 b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize)); 1646 if (b.getExpandedView() != null) { 1647 b.getExpandedView().updateDimensions(); 1648 } 1649 } 1650 if (mShowingOverflow) { 1651 mBubbleOverflow.getIconView().setLayoutParams( 1652 new LayoutParams(mBubbleSize, mBubbleSize)); 1653 } 1654 mExpandedAnimationController.updateResources(); 1655 mStackAnimationController.updateResources(); 1656 mDismissView.updateResources(); 1657 mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2); 1658 if (!isStackEduVisible()) { 1659 mStackAnimationController.setStackPosition( 1660 new RelativeStackPosition( 1661 mPositioner.getRestingPosition(), 1662 mPositioner.getAllowableStackPositionRegion(getBubbleCount()))); 1663 } 1664 if (mIsExpanded) { 1665 updateExpandedView(); 1666 } 1667 setUpManageMenu(); 1668 if (mShowingManage) { 1669 // the manage menu location depends on the manage button location which may need a 1670 // layout pass, so post this to the looper 1671 post(() -> showManageMenu(true)); 1672 } 1673 } 1674 1675 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)1676 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 1677 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 1678 1679 mTempRect.setEmpty(); 1680 getTouchableRegion(mTempRect); 1681 inoutInfo.touchableRegion.set(mTempRect); 1682 } 1683 1684 @Override onAttachedToWindow()1685 protected void onAttachedToWindow() { 1686 super.onAttachedToWindow(); 1687 WindowManager windowManager = mContext.getSystemService(WindowManager.class); 1688 mPositioner.update(DeviceConfig.create(mContext, Objects.requireNonNull(windowManager))); 1689 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 1690 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); 1691 } 1692 1693 @Override onDetachedFromWindow()1694 protected void onDetachedFromWindow() { 1695 super.onDetachedFromWindow(); 1696 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); 1697 getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); 1698 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 1699 } 1700 1701 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1702 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1703 super.onInitializeAccessibilityNodeInfoInternal(info); 1704 setupLocalMenu(info); 1705 } 1706 updateExpandedViewTheme()1707 void updateExpandedViewTheme() { 1708 final List<Bubble> bubbles = mBubbleData.getBubbles(); 1709 if (bubbles.isEmpty()) { 1710 return; 1711 } 1712 bubbles.forEach(bubble -> { 1713 if (bubble.getExpandedView() != null) { 1714 bubble.getExpandedView().applyThemeAttrs(); 1715 } 1716 }); 1717 } 1718 updateExpandedViewDirection(int direction)1719 void updateExpandedViewDirection(int direction) { 1720 final List<Bubble> bubbles = mBubbleData.getBubbles(); 1721 if (bubbles.isEmpty()) { 1722 return; 1723 } 1724 bubbles.forEach(bubble -> { 1725 if (bubble.getExpandedView() != null) { 1726 bubble.getExpandedView().setLayoutDirection(direction); 1727 } 1728 }); 1729 } 1730 setupLocalMenu(AccessibilityNodeInfo info)1731 void setupLocalMenu(AccessibilityNodeInfo info) { 1732 Resources res = mContext.getResources(); 1733 1734 // Custom local actions. 1735 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left, 1736 res.getString(R.string.bubble_accessibility_action_move_top_left)); 1737 info.addAction(moveTopLeft); 1738 1739 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right, 1740 res.getString(R.string.bubble_accessibility_action_move_top_right)); 1741 info.addAction(moveTopRight); 1742 1743 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left, 1744 res.getString(R.string.bubble_accessibility_action_move_bottom_left)); 1745 info.addAction(moveBottomLeft); 1746 1747 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right, 1748 res.getString(R.string.bubble_accessibility_action_move_bottom_right)); 1749 info.addAction(moveBottomRight); 1750 1751 // Default actions. 1752 info.addAction(AccessibilityAction.ACTION_DISMISS); 1753 if (mIsExpanded) { 1754 info.addAction(AccessibilityAction.ACTION_COLLAPSE); 1755 } else { 1756 info.addAction(AccessibilityAction.ACTION_EXPAND); 1757 } 1758 } 1759 1760 @Override performAccessibilityActionInternal(int action, Bundle arguments)1761 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1762 if (super.performAccessibilityActionInternal(action, arguments)) { 1763 return true; 1764 } 1765 final RectF stackBounds = mPositioner.getAllowableStackPositionRegion(getBubbleCount()); 1766 1767 // R constants are not final so we cannot use switch-case here. 1768 if (action == AccessibilityNodeInfo.ACTION_DISMISS) { 1769 mBubbleData.dismissAll(Bubbles.DISMISS_ACCESSIBILITY_ACTION); 1770 announceForAccessibility( 1771 getResources().getString(R.string.accessibility_bubble_dismissed)); 1772 return true; 1773 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { 1774 mBubbleData.setExpanded(false); 1775 return true; 1776 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) { 1777 mBubbleData.setExpanded(true); 1778 return true; 1779 } else if (action == R.id.action_move_top_left) { 1780 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top); 1781 return true; 1782 } else if (action == R.id.action_move_top_right) { 1783 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top); 1784 return true; 1785 } else if (action == R.id.action_move_bottom_left) { 1786 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom); 1787 return true; 1788 } else if (action == R.id.action_move_bottom_right) { 1789 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom); 1790 return true; 1791 } 1792 return false; 1793 } 1794 1795 /** 1796 * Update content description for a11y TalkBack. 1797 */ updateContentDescription()1798 public void updateContentDescription() { 1799 if (mBubbleData.getBubbles().isEmpty()) { 1800 return; 1801 } 1802 1803 for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { 1804 final Bubble bubble = mBubbleData.getBubbles().get(i); 1805 final String appName = bubble.getAppName(); 1806 1807 String titleStr = bubble.getTitle(); 1808 if (titleStr == null) { 1809 titleStr = getResources().getString(R.string.notification_bubble_title); 1810 } 1811 1812 if (bubble.getIconView() != null) { 1813 if (mIsExpanded || i > 0) { 1814 bubble.getIconView().setContentDescription(getResources().getString( 1815 R.string.bubble_content_description_single, titleStr, appName)); 1816 } else { 1817 final int moreCount = getBubbleCount(); 1818 bubble.getIconView().setContentDescription(getResources().getString( 1819 R.string.bubble_content_description_stack, 1820 titleStr, appName, moreCount)); 1821 } 1822 } 1823 } 1824 } 1825 1826 /** 1827 * Update bubbles' icon views accessibility states. 1828 */ updateBubblesAcessibillityStates()1829 public void updateBubblesAcessibillityStates() { 1830 for (int i = 0; i < mBubbleData.getBubbles().size(); i++) { 1831 Bubble prevBubble = i > 0 ? mBubbleData.getBubbles().get(i - 1) : null; 1832 Bubble bubble = mBubbleData.getBubbles().get(i); 1833 1834 View bubbleIconView = bubble.getIconView(); 1835 if (bubbleIconView == null) { 1836 continue; 1837 } 1838 1839 if (mIsExpanded) { 1840 // when stack is expanded 1841 // all bubbles are important for accessibility 1842 bubbleIconView 1843 .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 1844 1845 View prevBubbleIconView = prevBubble != null ? prevBubble.getIconView() : null; 1846 1847 if (prevBubbleIconView != null) { 1848 bubbleIconView.setAccessibilityDelegate(new View.AccessibilityDelegate() { 1849 @Override 1850 public void onInitializeAccessibilityNodeInfo(View v, 1851 AccessibilityNodeInfo info) { 1852 super.onInitializeAccessibilityNodeInfo(v, info); 1853 info.setTraversalAfter(prevBubbleIconView); 1854 } 1855 }); 1856 } 1857 } else { 1858 // when stack is collapsed, only the top bubble is important for accessibility, 1859 bubbleIconView.setImportantForAccessibility( 1860 i == 0 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : 1861 View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1862 } 1863 } 1864 1865 if (mIsExpanded) { 1866 // make the overflow bubble last in the accessibility traversal order 1867 1868 View bubbleOverflowIconView = 1869 mBubbleOverflow != null ? mBubbleOverflow.getIconView() : null; 1870 if (mShowingOverflow && bubbleOverflowIconView != null 1871 && !mBubbleData.getBubbles().isEmpty()) { 1872 Bubble lastBubble = 1873 mBubbleData.getBubbles().get(mBubbleData.getBubbles().size() - 1); 1874 View lastBubbleIconView = lastBubble.getIconView(); 1875 if (lastBubbleIconView != null) { 1876 bubbleOverflowIconView.setAccessibilityDelegate( 1877 new View.AccessibilityDelegate() { 1878 @Override 1879 public void onInitializeAccessibilityNodeInfo(View v, 1880 AccessibilityNodeInfo info) { 1881 super.onInitializeAccessibilityNodeInfo(v, info); 1882 info.setTraversalAfter(lastBubbleIconView); 1883 } 1884 }); 1885 } 1886 } 1887 } 1888 } 1889 updateSystemGestureExcludeRects()1890 private void updateSystemGestureExcludeRects() { 1891 // Exclude the region occupied by the first BubbleView in the stack 1892 Rect excludeZone = mSystemGestureExclusionRects.get(0); 1893 if (getBubbleCount() > 0) { 1894 View firstBubble = mBubbleContainer.getChildAt(0); 1895 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(), 1896 firstBubble.getBottom()); 1897 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f), 1898 (int) (firstBubble.getTranslationY() + 0.5f)); 1899 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects); 1900 } else { 1901 excludeZone.setEmpty(); 1902 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList()); 1903 } 1904 } 1905 1906 /** 1907 * Sets the listener to notify when the bubble stack is expanded. 1908 */ setExpandListener(Bubbles.BubbleExpandListener listener)1909 public void setExpandListener(Bubbles.BubbleExpandListener listener) { 1910 mExpandListener = listener; 1911 } 1912 1913 /** Sets the function to call to un-bubble the given conversation. */ setUnbubbleConversationCallback( Consumer<String> unbubbleConversationCallback)1914 public void setUnbubbleConversationCallback( 1915 Consumer<String> unbubbleConversationCallback) { 1916 mUnbubbleConversationCallback = unbubbleConversationCallback; 1917 } 1918 1919 /** 1920 * Whether the stack of bubbles is expanded or not. 1921 */ isExpanded()1922 public boolean isExpanded() { 1923 return mIsExpanded; 1924 } 1925 1926 /** 1927 * Whether the stack of bubbles is animating to or from expansion. 1928 */ isExpansionAnimating()1929 public boolean isExpansionAnimating() { 1930 return mIsExpansionAnimating; 1931 } 1932 1933 /** 1934 * Whether the stack of bubbles is animating a switch between bubbles. 1935 */ isSwitchAnimating()1936 public boolean isSwitchAnimating() { 1937 return mIsBubbleSwitchAnimating; 1938 } 1939 1940 /** 1941 * The {@link Bubble} that is expanded, null if one does not exist. 1942 */ 1943 @VisibleForTesting 1944 @Nullable getExpandedBubble()1945 public BubbleViewProvider getExpandedBubble() { 1946 return mExpandedBubble; 1947 } 1948 1949 @Nullable getExpandedView()1950 private BubbleExpandedView getExpandedView() { 1951 return mExpandedBubble != null ? mExpandedBubble.getExpandedView() : null; 1952 } 1953 1954 // via BubbleData.Listener 1955 @SuppressLint("ClickableViewAccessibility") addBubble(Bubble bubble)1956 void addBubble(Bubble bubble) { 1957 final boolean firstBubble = getBubbleCount() == 0; 1958 1959 if (firstBubble && shouldShowStackEdu()) { 1960 // Override the default stack position if we're showing user education. 1961 mStackAnimationController.setStackPosition(mPositioner.getDefaultStartPosition()); 1962 } 1963 1964 if (bubble.getIconView() == null) { 1965 return; 1966 } 1967 1968 if (firstBubble && bubble.isAppBubble() && !mPositioner.hasUserModifiedDefaultPosition()) { 1969 // TODO (b/294284894): update language around "app bubble" here 1970 // If it's an app bubble and we don't have a previous resting position, update the 1971 // controllers to use the default position for the app bubble (it'd be different from 1972 // the position initialized with the controllers originally). 1973 PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */); 1974 mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition); 1975 mStackAnimationController.setStackPosition(startPosition); 1976 mExpandedAnimationController.setCollapsePoint(startPosition); 1977 } else if (firstBubble) { 1978 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 1979 } 1980 1981 // Set the view translation x so that this bubble will animate in from the same side they 1982 // expand / collapse on. 1983 bubble.getIconView().setTranslationX(mStackAnimationController.getStackPosition().x); 1984 1985 mBubbleContainer.addView(bubble.getIconView(), 0, 1986 new FrameLayout.LayoutParams(mPositioner.getBubbleSize(), 1987 mPositioner.getBubbleSize())); 1988 1989 // Set the dot position to the opposite of the side the stack is resting on, since the stack 1990 // resting slightly off-screen would result in the dot also being off-screen. 1991 bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */); 1992 bubble.getIconView().setOnClickListener(mBubbleClickListener); 1993 bubble.getIconView().setOnTouchListener(mBubbleTouchListener); 1994 updateBubbleShadows(mIsExpanded); 1995 animateInFlyoutForBubble(bubble); 1996 requestUpdate(); 1997 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED); 1998 } 1999 2000 // via BubbleData.Listener removeBubble(Bubble bubble)2001 void removeBubble(Bubble bubble) { 2002 if (isExpanded() && getBubbleCount() == 1) { 2003 mRemovingLastBubbleWhileExpanded = true; 2004 // We're expanded while the last bubble is being removed. Let the scrim animate away 2005 // and then remove our views (removing the icon view triggers the removal of the 2006 // bubble window so do that at the end of the animation so we see the scrim animate). 2007 BadgedImageView iconView = bubble.getIconView(); 2008 showScrim(false, () -> { 2009 mRemovingLastBubbleWhileExpanded = false; 2010 bubble.cleanupExpandedView(); 2011 if (iconView != null) { 2012 mBubbleContainer.removeView(iconView); 2013 } 2014 bubble.cleanupViews(); // cleans up the icon view 2015 updateExpandedView(); // resets state for no expanded bubble 2016 mExpandedBubble = null; 2017 }); 2018 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); 2019 return; 2020 } else if (getBubbleCount() == 1) { 2021 mExpandedBubble = null; 2022 } 2023 // Remove it from the views 2024 for (int i = 0; i < getBubbleCount(); i++) { 2025 View v = mBubbleContainer.getChildAt(i); 2026 if (v instanceof BadgedImageView 2027 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) { 2028 mBubbleContainer.removeViewAt(i); 2029 if (mBubbleData.hasOverflowBubbleWithKey(bubble.getKey())) { 2030 bubble.cleanupExpandedView(); 2031 } else { 2032 bubble.cleanupViews(); 2033 } 2034 updateExpandedView(); 2035 if (getBubbleCount() == 0 && !isExpanded()) { 2036 // This is the last bubble and the stack is collapsed 2037 updateStackPosition(); 2038 } 2039 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); 2040 return; 2041 } 2042 } 2043 // If a bubble is suppressed, it is not attached to the container. Clean it up. 2044 if (bubble.isSuppressed()) { 2045 bubble.cleanupViews(); 2046 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); 2047 } else { 2048 Log.w(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble); 2049 } 2050 } 2051 2052 // via BubbleData.Listener updateBubble(Bubble bubble)2053 void updateBubble(Bubble bubble) { 2054 animateInFlyoutForBubble(bubble); 2055 requestUpdate(); 2056 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); 2057 } 2058 2059 /** 2060 * Update bubble order and pointer position. 2061 */ updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPosition)2062 public void updateBubbleOrder(List<Bubble> bubbles, boolean updatePointerPosition) { 2063 // Don't reorder bubbles in the middle of a gesture because that would remove bubbles from 2064 // view hierarchy and will cancel all touch events. Instead wait until the gesture is 2065 // finished and then reorder. 2066 if (mIsGestureInProgress) { 2067 mShouldReorderBubblesAfterGestureCompletes = true; 2068 return; 2069 } 2070 updateBubbleOrderInternal(bubbles, updatePointerPosition); 2071 } 2072 updateBubbleOrderInternal(List<Bubble> bubbles, boolean updatePointerPosition)2073 private void updateBubbleOrderInternal(List<Bubble> bubbles, boolean updatePointerPosition) { 2074 final Runnable reorder = () -> { 2075 for (int i = 0; i < bubbles.size(); i++) { 2076 Bubble bubble = bubbles.get(i); 2077 mBubbleContainer.reorderView(bubble.getIconView(), i); 2078 } 2079 }; 2080 if (mIsExpanded || isExpansionAnimating()) { 2081 reorder.run(); 2082 updateBadges(false /* setBadgeForCollapsedStack */); 2083 updateBubbleShadows(true /* isExpanded */); 2084 } else { 2085 List<View> bubbleViews = bubbles.stream() 2086 .map(b -> b.getIconView()).collect(Collectors.toList()); 2087 mStackAnimationController.animateReorder(bubbleViews, reorder); 2088 } 2089 2090 if (updatePointerPosition) { 2091 updatePointerPosition(false /* forIme */); 2092 } 2093 } 2094 2095 /** 2096 * Changes the currently selected bubble. If the stack is already expanded, the newly selected 2097 * bubble will be shown immediately. This does not change the expanded state or change the 2098 * position of any bubble. 2099 */ 2100 // via BubbleData.Listener setSelectedBubble(@ullable BubbleViewProvider bubbleToSelect)2101 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) { 2102 if (bubbleToSelect == null) { 2103 mBubbleData.setShowingOverflow(false); 2104 return; 2105 } 2106 2107 // Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want 2108 // to re-render it even if it has the same key (equals() returns true). If the currently 2109 // expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance 2110 // with the same key (with newly inflated expanded views), and we need to render those new 2111 // views. 2112 if (mExpandedBubble == bubbleToSelect) { 2113 return; 2114 } 2115 2116 if (bubbleToSelect.getKey().equals(BubbleOverflow.KEY)) { 2117 mBubbleData.setShowingOverflow(true); 2118 } else { 2119 mBubbleData.setShowingOverflow(false); 2120 } 2121 2122 if (mIsExpanded && mIsExpansionAnimating) { 2123 // If the bubble selection changed during the expansion animation, the expanding bubble 2124 // probably crashed or immediately removed itself (or, we just got unlucky with a new 2125 // auto-expanding bubble showing up at just the right time). Cancel the animations so we 2126 // can start fresh. 2127 cancelAllExpandCollapseSwitchAnimations(); 2128 } 2129 showManageMenu(false /* show */); 2130 2131 // If we're expanded, screenshot the currently expanded bubble (before expanding the newly 2132 // selected bubble) so we can animate it out. 2133 BubbleExpandedView expandedView = getExpandedView(); 2134 if (mIsExpanded && expandedView != null && !mExpandedViewTemporarilyHidden) { 2135 // Before screenshotting, have the real TaskView show on top of other surfaces 2136 // so that the screenshot doesn't flicker on top of it. 2137 expandedView.setSurfaceZOrderedOnTop(true); 2138 2139 try { 2140 screenshotAnimatingOutBubbleIntoSurface((success) -> { 2141 mAnimatingOutSurfaceContainer.setVisibility( 2142 success ? View.VISIBLE : View.INVISIBLE); 2143 showNewlySelectedBubble(bubbleToSelect); 2144 }); 2145 } catch (Exception e) { 2146 showNewlySelectedBubble(bubbleToSelect); 2147 e.printStackTrace(); 2148 } 2149 } else { 2150 showNewlySelectedBubble(bubbleToSelect); 2151 } 2152 } 2153 showNewlySelectedBubble(BubbleViewProvider bubbleToSelect)2154 private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { 2155 final BubbleViewProvider previouslySelected = mExpandedBubble; 2156 mExpandedBubble = bubbleToSelect; 2157 mExpandedViewAnimationController.setExpandedView(getExpandedView()); 2158 2159 if (mIsExpanded) { 2160 hideCurrentInputMethod(); 2161 2162 if (Flags.enableRetrievableBubbles()) { 2163 if (mBubbleData.getBubbles().size() == 1) { 2164 // First bubble, check if overflow visibility needs to change 2165 updateOverflowVisibility(); 2166 } 2167 } 2168 2169 // Make the container of the expanded view transparent before removing the expanded view 2170 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the 2171 // expanded view becomes visible on the screen. See b/126856255 2172 mExpandedViewContainer.setAlpha(0.0f); 2173 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 2174 if (previouslySelected != null) { 2175 previouslySelected.setTaskViewVisibility(false); 2176 } 2177 2178 updateExpandedBubble(); 2179 requestUpdate(); 2180 2181 logBubbleEvent(previouslySelected, 2182 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 2183 logBubbleEvent(bubbleToSelect, 2184 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 2185 notifyExpansionChanged(previouslySelected, false /* expanded */); 2186 notifyExpansionChanged(bubbleToSelect, true /* expanded */); 2187 }); 2188 } 2189 } 2190 2191 /** 2192 * Changes the expanded state of the stack. 2193 * Don't call this directly, call mBubbleData#setExpanded. 2194 * 2195 * @param shouldExpand whether the bubble stack should appear expanded 2196 */ 2197 // via BubbleData.Listener setExpanded(boolean shouldExpand)2198 public void setExpanded(boolean shouldExpand) { 2199 if (!shouldExpand) { 2200 // If we're collapsing, release the animating-out surface immediately since we have no 2201 // need for it, and this ensures it cannot remain visible as we collapse. 2202 releaseAnimatingOutBubbleBuffer(); 2203 } 2204 2205 if (shouldExpand == mIsExpanded) { 2206 return; 2207 } 2208 2209 boolean wasExpanded = mIsExpanded; 2210 2211 hideCurrentInputMethod(); 2212 2213 mSysuiProxyProvider.getSysuiProxy().onStackExpandChanged(shouldExpand); 2214 2215 if (wasExpanded) { 2216 stopMonitoringSwipeUpGesture(); 2217 animateCollapse(); 2218 showManageMenu(false); 2219 logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED); 2220 } else { 2221 animateExpansion(); 2222 // TODO: move next line to BubbleData 2223 logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); 2224 logBubbleEvent(mExpandedBubble, 2225 FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); 2226 mManager.checkNotificationPanelExpandedState(notifPanelExpanded -> { 2227 if (!notifPanelExpanded && mIsExpanded) { 2228 startMonitoringSwipeUpGesture(); 2229 } 2230 }); 2231 } 2232 notifyExpansionChanged(mExpandedBubble, mIsExpanded); 2233 announceExpandForAccessibility(mExpandedBubble, mIsExpanded); 2234 } 2235 2236 /** 2237 * Check if we only have overflow expanded. Which is the case when we are launching bubbles from 2238 * background. 2239 */ isOnlyOverflowExpanded()2240 private boolean isOnlyOverflowExpanded() { 2241 boolean overflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals( 2242 mExpandedBubble.getKey()); 2243 return overflowExpanded && !mBubbleData.hasBubbles(); 2244 } 2245 2246 /** 2247 * Monitor for swipe up gesture that is used to collapse expanded view 2248 */ startMonitoringSwipeUpGesture()2249 void startMonitoringSwipeUpGesture() { 2250 stopMonitoringSwipeUpGestureInternal(); 2251 2252 if (isGestureNavEnabled()) { 2253 mBubblesNavBarGestureTracker = new BubblesNavBarGestureTracker(mContext, mPositioner); 2254 mBubblesNavBarGestureTracker.start(mSwipeUpListener); 2255 setOnTouchListener(mContainerSwipeListener); 2256 } 2257 } 2258 announceExpandForAccessibility(BubbleViewProvider bubble, boolean expanded)2259 private void announceExpandForAccessibility(BubbleViewProvider bubble, boolean expanded) { 2260 if (bubble instanceof Bubble) { 2261 String contentDescription = getBubbleContentDescription((Bubble) bubble); 2262 String message = getResources().getString( 2263 expanded 2264 ? R.string.bubble_accessibility_announce_expand 2265 : R.string.bubble_accessibility_announce_collapse, contentDescription); 2266 announceForAccessibility(message); 2267 } 2268 } 2269 2270 @NonNull getBubbleContentDescription(Bubble bubble)2271 private String getBubbleContentDescription(Bubble bubble) { 2272 final String appName = bubble.getAppName(); 2273 final String title = bubble.getTitle() != null 2274 ? bubble.getTitle() 2275 : getResources().getString(R.string.notification_bubble_title); 2276 2277 if (appName == null || title.equals(appName)) { 2278 // App bubble title equals the app name, so return only the title to avoid having 2279 // content description like: `<app> from <app>`. 2280 return title; 2281 } else { 2282 return getResources().getString( 2283 R.string.bubble_content_description_single, title, appName); 2284 } 2285 } 2286 isGestureNavEnabled()2287 private boolean isGestureNavEnabled() { 2288 return mContext.getResources().getInteger( 2289 com.android.internal.R.integer.config_navBarInteractionMode) 2290 == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; 2291 } 2292 2293 /** 2294 * Stop monitoring for swipe up gesture 2295 */ stopMonitoringSwipeUpGesture()2296 void stopMonitoringSwipeUpGesture() { 2297 stopMonitoringSwipeUpGestureInternal(); 2298 } 2299 stopMonitoringSwipeUpGestureInternal()2300 private void stopMonitoringSwipeUpGestureInternal() { 2301 if (mBubblesNavBarGestureTracker != null) { 2302 mBubblesNavBarGestureTracker.stop(); 2303 mBubblesNavBarGestureTracker = null; 2304 setOnTouchListener(null); 2305 } 2306 } 2307 2308 /** 2309 * Called when back press occurs while bubbles are expanded. 2310 */ onBackPressed()2311 public void onBackPressed() { 2312 if (mIsExpanded) { 2313 if (mShowingManage) { 2314 showManageMenu(false); 2315 } else if (isManageEduVisible()) { 2316 mManageEduView.hide(); 2317 } else { 2318 mBubbleData.setExpanded(false); 2319 } 2320 } 2321 } 2322 setBubbleSuppressed(Bubble bubble, boolean suppressed)2323 void setBubbleSuppressed(Bubble bubble, boolean suppressed) { 2324 if (suppressed) { 2325 int index = getBubbleIndex(bubble); 2326 mBubbleContainer.removeViewAt(index); 2327 updateExpandedView(); 2328 } else { 2329 if (bubble.getIconView() == null) { 2330 return; 2331 } 2332 if (bubble.getIconView().getParent() != null) { 2333 Log.e(TAG, "Bubble is already added to parent. Can't unsuppress: " + bubble); 2334 return; 2335 } 2336 int index = mBubbleData.getBubbles().indexOf(bubble); 2337 // Add the view back to the correct position 2338 mBubbleContainer.addView(bubble.getIconView(), index, 2339 new LayoutParams(mPositioner.getBubbleSize(), 2340 mPositioner.getBubbleSize())); 2341 updateBubbleShadows(mIsExpanded); 2342 requestUpdate(); 2343 } 2344 } 2345 onSensitiveNotificationProtectionStateChanged( boolean sensitiveNotificationProtectionActive)2346 void onSensitiveNotificationProtectionStateChanged( 2347 boolean sensitiveNotificationProtectionActive) { 2348 mSensitiveNotificationProtectionActive = sensitiveNotificationProtectionActive; 2349 } 2350 2351 /** 2352 * Asks the BubbleController to hide the IME from anywhere, whether it's focused on Bubbles or 2353 * not. 2354 */ hideCurrentInputMethod()2355 void hideCurrentInputMethod() { 2356 mManager.hideCurrentInputMethod(); 2357 } 2358 2359 /** Set the stack position to whatever the positioner says. */ updateStackPosition()2360 void updateStackPosition() { 2361 mStackAnimationController.setStackPosition(mPositioner.getRestingPosition()); 2362 mDismissView.hide(); 2363 } 2364 beforeExpandedViewAnimation()2365 private void beforeExpandedViewAnimation() { 2366 mIsExpansionAnimating = true; 2367 hideFlyoutImmediate(); 2368 updateExpandedBubble(); 2369 updateExpandedView(); 2370 } 2371 afterExpandedViewAnimation()2372 private void afterExpandedViewAnimation() { 2373 mIsExpansionAnimating = false; 2374 updateExpandedView(); 2375 requestUpdate(); 2376 } 2377 2378 /** Animate the expanded view hidden. This is done while we're dragging out a bubble. */ hideExpandedViewIfNeeded()2379 private void hideExpandedViewIfNeeded() { 2380 if (mExpandedViewTemporarilyHidden 2381 || mExpandedBubble == null 2382 || mExpandedBubble.getExpandedView() == null) { 2383 return; 2384 } 2385 2386 mExpandedViewTemporarilyHidden = true; 2387 2388 // Scale down. 2389 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2390 .spring(AnimatableScaleMatrix.SCALE_X, 2391 AnimatableScaleMatrix.getAnimatableValueForScaleFactor( 2392 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), 2393 mScaleOutSpringConfig) 2394 .spring(AnimatableScaleMatrix.SCALE_Y, 2395 AnimatableScaleMatrix.getAnimatableValueForScaleFactor( 2396 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT), 2397 mScaleOutSpringConfig) 2398 .addUpdateListener((target, values) -> 2399 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix)) 2400 .start(); 2401 2402 // Animate alpha from 1f to 0f. 2403 mExpandedViewAlphaAnimator.reverse(); 2404 } 2405 2406 /** 2407 * Animate the expanded view visible again. This is done when we're done dragging out a bubble. 2408 */ showExpandedViewIfNeeded()2409 private void showExpandedViewIfNeeded() { 2410 if (!mExpandedViewTemporarilyHidden) { 2411 return; 2412 } 2413 2414 mExpandedViewTemporarilyHidden = false; 2415 2416 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2417 .spring(AnimatableScaleMatrix.SCALE_X, 2418 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2419 mScaleOutSpringConfig) 2420 .spring(AnimatableScaleMatrix.SCALE_Y, 2421 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2422 mScaleOutSpringConfig) 2423 .addUpdateListener((target, values) -> 2424 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix)) 2425 .start(); 2426 2427 mExpandedViewAlphaAnimator.start(); 2428 } 2429 showScrim(boolean show, Runnable after)2430 private void showScrim(boolean show, Runnable after) { 2431 AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { 2432 @Override 2433 public void onAnimationEnd(Animator animation) { 2434 mScrimAnimation = null; 2435 if (after != null) { 2436 after.run(); 2437 } 2438 } 2439 }; 2440 if (mScrimAnimation != null) { 2441 // Cancel scrim animation if it animates 2442 mScrimAnimation.cancel(); 2443 } 2444 if (show) { 2445 mScrimAnimation = mScrim.animate(); 2446 mScrimAnimation 2447 .setInterpolator(ALPHA_IN) 2448 .alpha(BUBBLE_EXPANDED_SCRIM_ALPHA) 2449 .setListener(listener) 2450 .start(); 2451 } else { 2452 mScrimAnimation = mScrim.animate(); 2453 mScrimAnimation 2454 .alpha(0f) 2455 .setInterpolator(ALPHA_OUT) 2456 .setListener(listener) 2457 .start(); 2458 } 2459 } 2460 animateExpansion()2461 private void animateExpansion() { 2462 ProtoLog.d(WM_SHELL_BUBBLES, "animateExpansion, expandedBubble=%s", 2463 mExpandedBubble != null ? mExpandedBubble.getKey() : "null"); 2464 cancelDelayedExpandCollapseSwitchAnimations(); 2465 2466 mIsExpanded = true; 2467 if (isStackEduVisible()) { 2468 mStackEduView.hide(true /* fromExpansion */); 2469 } 2470 beforeExpandedViewAnimation(); 2471 2472 showScrim(true, null /* runnable */); 2473 updateBubbleShadows(mIsExpanded); 2474 mBubbleContainer.setActiveController(mExpandedAnimationController); 2475 updateOverflowVisibility(); 2476 2477 if (Flags.enableRetrievableBubbles() && isOnlyOverflowExpanded()) { 2478 animateOverflowExpansion(); 2479 } else { 2480 animateBubbleExpansion(); 2481 } 2482 } 2483 animateBubbleExpansion()2484 private void animateBubbleExpansion() { 2485 updateBadges(false /* setBadgeForCollapsedStack */); 2486 updatePointerPosition(false /* forIme */); 2487 if (Flags.enableBubbleStashing()) { 2488 mBubbleContainer.animate().translationX(0).start(); 2489 } 2490 mExpandedAnimationController.expandFromStack(() -> { 2491 if (mIsExpanded && getExpandedView() != null) { 2492 maybeShowManageEdu(); 2493 } 2494 updateOverflowDotVisibility(true /* expanding */); 2495 } /* after */); 2496 int index; 2497 if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { 2498 index = mBubbleData.getBubbles().size(); 2499 } else { 2500 index = getBubbleIndex(mExpandedBubble); 2501 } 2502 PointF bubbleXY = mPositioner.getExpandedBubbleXY(index, getState()); 2503 final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, 2504 mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x); 2505 mExpandedViewContainer.setTranslationX(0f); 2506 mExpandedViewContainer.setTranslationY(translationY); 2507 mExpandedViewContainer.setAlpha(1f); 2508 2509 final boolean showVertically = mPositioner.showBubblesVertically(); 2510 // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles 2511 // that are animating farther, so that the expanded view doesn't move as much. 2512 final float relevantStackPosition = showVertically 2513 ? mStackAnimationController.getStackPosition().y 2514 : mStackAnimationController.getStackPosition().x; 2515 final float bubbleWillBeAt = showVertically 2516 ? bubbleXY.y 2517 : bubbleXY.x; 2518 final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); 2519 2520 // Wait for the path animation target to reach its end, and add a small amount of extra time 2521 // if the bubble is moving a lot horizontally. 2522 final long startDelay; 2523 2524 // Should not happen since we lay out before expanding, but just in case... 2525 if (getWidth() > 0) { 2526 startDelay = (long) 2527 (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f 2528 + (distanceAnimated / getWidth()) * 30); 2529 } else { 2530 startDelay = 0L; 2531 } 2532 2533 // Set the pivot point for the scale, so the expanded view animates out from the bubble. 2534 if (showVertically) { 2535 float pivotX; 2536 if (mStackOnLeftOrWillBe) { 2537 pivotX = bubbleXY.x + mBubbleSize + mExpandedViewPadding; 2538 } else { 2539 pivotX = bubbleXY.x - mExpandedViewPadding; 2540 } 2541 mExpandedViewContainerMatrix.setScale( 2542 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2543 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2544 pivotX, 2545 bubbleXY.y + mBubbleSize / 2f); 2546 } else { 2547 mExpandedViewContainerMatrix.setScale( 2548 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2549 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2550 bubbleXY.x + mBubbleSize / 2f, 2551 bubbleXY.y + mBubbleSize + mExpandedViewPadding); 2552 } 2553 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2554 2555 BubbleExpandedView expandedView = getExpandedView(); 2556 if (expandedView != null) { 2557 expandedView.setContentAlpha(0f); 2558 expandedView.setBackgroundAlpha(0f); 2559 2560 // We'll be starting the alpha animation after a slight delay, so set this flag early 2561 // here. 2562 expandedView.setAnimating(true); 2563 } 2564 2565 mDelayedAnimation = () -> { 2566 mExpandedViewAlphaAnimator.start(); 2567 2568 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2569 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2570 .spring(AnimatableScaleMatrix.SCALE_X, 2571 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2572 mScaleInSpringConfig) 2573 .spring(AnimatableScaleMatrix.SCALE_Y, 2574 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2575 mScaleInSpringConfig) 2576 .addUpdateListener((target, values) -> { 2577 if (mExpandedBubble == null || mExpandedBubble.getIconView() == null) { 2578 return; 2579 } 2580 float translation = showVertically 2581 ? mExpandedBubble.getIconView().getTranslationY() 2582 : mExpandedBubble.getIconView().getTranslationX(); 2583 mExpandedViewContainerMatrix.postTranslate( 2584 translation - bubbleWillBeAt, 2585 0); 2586 mExpandedViewContainer.setAnimationMatrix( 2587 mExpandedViewContainerMatrix); 2588 }) 2589 .withEndActions(() -> { 2590 mExpandedViewContainer.setAnimationMatrix(null); 2591 afterExpandedViewAnimation(); 2592 BubbleExpandedView expView = getExpandedView(); 2593 if (expView != null) { 2594 expView.setSurfaceZOrderedOnTop(false); 2595 } 2596 }) 2597 .start(); 2598 }; 2599 mMainExecutor.executeDelayed(mDelayedAnimation, startDelay); 2600 } 2601 2602 /** 2603 * Animate expansion of overflow view when it is shown from the bubble shortcut. 2604 * <p> 2605 * Animates the view with a scale originating from the center of the view. 2606 */ animateOverflowExpansion()2607 private void animateOverflowExpansion() { 2608 PointF bubbleXY = mPositioner.getExpandedBubbleXY(0, getState()); 2609 final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, 2610 mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x); 2611 mExpandedViewContainer.setTranslationX(0f); 2612 mExpandedViewContainer.setTranslationY(translationY); 2613 mExpandedViewContainer.setAlpha(1f); 2614 2615 boolean stackOnLeft = mPositioner.isStackOnLeft(getStackPosition()); 2616 float width = mPositioner.getTaskViewContentWidth(stackOnLeft); 2617 float height = mPositioner.getExpandedViewHeight(mExpandedBubble); 2618 float scale = 1f - OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT; 2619 // Scale from the center of the view 2620 mExpandedViewContainerMatrix.setScale(scale, scale, width / 2f, height / 2f); 2621 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2622 mExpandedViewAlphaAnimator.start(); 2623 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2624 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2625 .spring(AnimatableScaleMatrix.SCALE_X, 2626 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2627 mScaleInSpringConfig) 2628 .spring(AnimatableScaleMatrix.SCALE_Y, 2629 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2630 mScaleInSpringConfig) 2631 .addUpdateListener((target, values) -> { 2632 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2633 }).withEndActions(() -> { 2634 mExpandedViewContainer.setAnimationMatrix(null); 2635 afterExpandedViewAnimation(); 2636 BubbleExpandedView expandedView = getExpandedView(); 2637 if (expandedView != null) { 2638 expandedView.setSurfaceZOrderedOnTop(false); 2639 } 2640 }).start(); 2641 } 2642 animateCollapse()2643 private void animateCollapse() { 2644 cancelDelayedExpandCollapseSwitchAnimations(); 2645 ProtoLog.d(WM_SHELL_BUBBLES, "animateCollapse"); 2646 if (isManageEduVisible()) { 2647 mManageEduView.hide(); 2648 } 2649 2650 mIsExpanded = false; 2651 mIsExpansionAnimating = true; 2652 2653 if (!mRemovingLastBubbleWhileExpanded) { 2654 // When we remove the last bubble it animates the scrim. 2655 showScrim(false, null /* runnable */); 2656 } 2657 2658 mBubbleContainer.cancelAllAnimations(); 2659 2660 // If we were in the middle of swapping, the animating-out surface would have been scaling 2661 // to zero - finish it off. 2662 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2663 mAnimatingOutSurfaceContainer.setScaleX(0f); 2664 mAnimatingOutSurfaceContainer.setScaleY(0f); 2665 2666 // Let the expanded animation controller know that it shouldn't animate child adds/reorders 2667 // since we're about to animate collapsed. 2668 mExpandedAnimationController.notifyPreparingToCollapse(); 2669 final PointF collapsePosition = mStackAnimationController 2670 .getStackPositionAlongNearestHorizontalEdge(); 2671 updateOverflowDotVisibility(false /* expanding */); 2672 final Runnable collapseBackToStack = () -> 2673 mExpandedAnimationController.collapseBackToStack( 2674 collapsePosition, 2675 /* fadeBubblesDuringCollapse= */ mRemovingLastBubbleWhileExpanded, 2676 () -> { 2677 mBubbleContainer.setActiveController(mStackAnimationController); 2678 updateOverflowVisibility(); 2679 animateShadows(); 2680 }); 2681 2682 final Runnable after = () -> { 2683 final BubbleViewProvider previouslySelected = mExpandedBubble; 2684 // TODO(b/231350255): investigate why this call is needed here 2685 beforeExpandedViewAnimation(); 2686 if (mManageEduView != null) { 2687 mManageEduView.hide(); 2688 } 2689 2690 updateBadges(true /* setBadgeForCollapsedStack */); 2691 afterExpandedViewAnimation(); 2692 if (previouslySelected != null) { 2693 previouslySelected.setTaskViewVisibility(false); 2694 } 2695 mExpandedViewAnimationController.reset(); 2696 animateStashedState(false /* stashImmediately */); 2697 }; 2698 mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after, 2699 collapsePosition); 2700 BubbleExpandedView expandedView = getExpandedView(); 2701 if (expandedView != null) { 2702 // When the animation completes, we should no longer be showing the content. 2703 // This won't actually update content visibility immediately, if we are currently 2704 // animating. But updates the internal state for the content to be hidden after 2705 // animation completes. 2706 expandedView.setContentVisibility(false); 2707 } 2708 } 2709 animateSwitchBubbles()2710 private void animateSwitchBubbles() { 2711 // If we're no longer expanded, this is meaningless. 2712 if (!mIsExpanded) { 2713 mIsBubbleSwitchAnimating = false; 2714 return; 2715 } 2716 2717 // The surface contains a screenshot of the animating out bubble, so we just need to animate 2718 // it out (and then release the GraphicBuffer). 2719 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 2720 2721 mAnimatingOutSurfaceAlphaAnimator.reverse(); 2722 mExpandedViewAlphaAnimator.start(); 2723 2724 if (mPositioner.showBubblesVertically()) { 2725 float translationX = mStackAnimationController.isStackOnLeftSide() 2726 ? mAnimatingOutSurfaceContainer.getTranslationX() + mBubbleSize * 2 2727 : mAnimatingOutSurfaceContainer.getTranslationX(); 2728 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) 2729 .spring(DynamicAnimation.TRANSLATION_X, translationX, mTranslateSpringConfig) 2730 .start(); 2731 } else { 2732 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer) 2733 .spring(DynamicAnimation.TRANSLATION_Y, 2734 mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize, 2735 mTranslateSpringConfig) 2736 .start(); 2737 } 2738 2739 boolean isOverflow = mExpandedBubble != null 2740 && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); 2741 PointF p = mPositioner.getExpandedBubbleXY(isOverflow 2742 ? mBubbleContainer.getChildCount() - 1 2743 : mBubbleData.getBubbles().indexOf(mExpandedBubble), 2744 getState()); 2745 mExpandedViewContainer.setAlpha(1f); 2746 mExpandedViewContainer.setVisibility(View.VISIBLE); 2747 2748 if (mPositioner.showBubblesVertically()) { 2749 float pivotX; 2750 float pivotY = p.y + mBubbleSize / 2f; 2751 if (mStackOnLeftOrWillBe) { 2752 pivotX = p.x + mBubbleSize + mExpandedViewPadding; 2753 } else { 2754 pivotX = p.x - mExpandedViewPadding; 2755 } 2756 mExpandedViewContainerMatrix.setScale( 2757 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2758 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2759 pivotX, pivotY); 2760 } else { 2761 mExpandedViewContainerMatrix.setScale( 2762 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2763 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 2764 p.x + mBubbleSize / 2f, 2765 p.y + mBubbleSize + mExpandedViewPadding); 2766 } 2767 2768 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2769 2770 mMainExecutor.executeDelayed(() -> { 2771 if (!mIsExpanded) { 2772 mIsBubbleSwitchAnimating = false; 2773 return; 2774 } 2775 2776 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2777 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) 2778 .spring(AnimatableScaleMatrix.SCALE_X, 2779 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2780 mScaleInSpringConfig) 2781 .spring(AnimatableScaleMatrix.SCALE_Y, 2782 AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), 2783 mScaleInSpringConfig) 2784 .addUpdateListener((target, values) -> { 2785 mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); 2786 }) 2787 .withEndActions(() -> { 2788 mExpandedViewTemporarilyHidden = false; 2789 mIsBubbleSwitchAnimating = false; 2790 mExpandedViewContainer.setAnimationMatrix(null); 2791 2792 // When a bubble is being dragged, the expanded view is temporarily hidden. 2793 // If the motion ends with dismissing the bubble, with multiple bubbles in 2794 // the stack, we'll end up here to switch to the new bubble. However, the 2795 // expanded view animation might not actually set the z ordering for the 2796 // expanded view correctly, because the view may still be temporarily 2797 // hidden. So set it again here. 2798 BubbleExpandedView expandedView = getExpandedView(); 2799 if (expandedView != null) { 2800 expandedView.setSurfaceZOrderedOnTop(false); 2801 expandedView.setAnimating(false); 2802 } 2803 }) 2804 .start(); 2805 }, 25); 2806 } 2807 2808 /** 2809 * Cancels any delayed steps for expand/collapse and bubble switch animations, and resets the is 2810 * animating flags for those animations. 2811 */ cancelDelayedExpandCollapseSwitchAnimations()2812 private void cancelDelayedExpandCollapseSwitchAnimations() { 2813 mMainExecutor.removeCallbacks(mDelayedAnimation); 2814 2815 mIsExpansionAnimating = false; 2816 mIsBubbleSwitchAnimating = false; 2817 } 2818 cancelAllExpandCollapseSwitchAnimations()2819 private void cancelAllExpandCollapseSwitchAnimations() { 2820 cancelDelayedExpandCollapseSwitchAnimations(); 2821 2822 PhysicsAnimator.getInstance(mAnimatingOutSurfaceView).cancel(); 2823 PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); 2824 2825 mExpandedViewContainer.setAnimationMatrix(null); 2826 } 2827 notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded)2828 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) { 2829 if (mExpandListener != null && bubble != null) { 2830 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey()); 2831 } 2832 } 2833 2834 /** 2835 * Updates the stack based for IME changes. When collapsed it'll move the stack if it 2836 * overlaps where they IME would be. When expanded it'll shift the expanded bubbles 2837 * if they might overlap with the IME (this only happens for large screens) 2838 * and clip the expanded view. 2839 */ setImeVisible(boolean visible)2840 public void setImeVisible(boolean visible) { 2841 if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) { 2842 // This will update the animation so the bubbles move to position for the IME 2843 mExpandedAnimationController.expandFromStack(() -> { 2844 updatePointerPosition(false /* forIme */); 2845 afterExpandedViewAnimation(); 2846 mExpandedViewContainer.setVisibility(VISIBLE); 2847 mExpandedViewAnimationController.animateForImeVisibilityChange(visible); 2848 } /* after */); 2849 return; 2850 } 2851 2852 if (!mIsExpanded && getBubbleCount() > 0) { 2853 final float stackDestinationY = 2854 mStackAnimationController.animateForImeVisibility(visible); 2855 2856 // How far the stack is animating due to IME, we'll just animate the flyout by that 2857 // much too. 2858 final float stackDy = 2859 stackDestinationY - mStackAnimationController.getStackPosition().y; 2860 2861 // If the flyout is visible, translate it along with the bubble stack. 2862 if (mFlyout.getVisibility() == VISIBLE) { 2863 PhysicsAnimator.getInstance(mFlyout) 2864 .spring(DynamicAnimation.TRANSLATION_Y, 2865 mFlyout.getTranslationY() + stackDy, 2866 FLYOUT_IME_ANIMATION_SPRING_CONFIG) 2867 .start(); 2868 } 2869 } 2870 2871 if (mIsExpanded) { 2872 mExpandedViewAnimationController.animateForImeVisibilityChange(visible); 2873 BubbleExpandedView expandedView = getExpandedView(); 2874 if (mPositioner.showBubblesVertically() && expandedView != null) { 2875 float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex, 2876 getState()).y; 2877 float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY); 2878 expandedView.setImeVisible(visible); 2879 if (!expandedView.isUsingMaxHeight()) { 2880 mExpandedViewContainer.animate().translationY(newExpandedViewTop); 2881 } 2882 List<Animator> animList = new ArrayList<>(); 2883 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { 2884 View child = mBubbleContainer.getChildAt(i); 2885 float transY = mPositioner.getExpandedBubbleXY(i, getState()).y; 2886 ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY); 2887 animList.add(anim); 2888 } 2889 updatePointerPosition(true /* forIme */); 2890 AnimatorSet set = new AnimatorSet(); 2891 set.playTogether(animList); 2892 set.start(); 2893 } 2894 } 2895 } 2896 2897 @Override dispatchTouchEvent(MotionEvent ev)2898 public boolean dispatchTouchEvent(MotionEvent ev) { 2899 if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { 2900 // Ignore touches from additional pointer indices. 2901 return false; 2902 } 2903 2904 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 2905 mPointerIndexDown = ev.getActionIndex(); 2906 } else if (ev.getAction() == MotionEvent.ACTION_UP 2907 || ev.getAction() == MotionEvent.ACTION_CANCEL) { 2908 mPointerIndexDown = -1; 2909 } 2910 2911 boolean dispatched = super.dispatchTouchEvent(ev); 2912 2913 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned 2914 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will 2915 // then be passed to the new bubble, which will not consume them since it hasn't received an 2916 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler 2917 // until the current gesture ends with an ACTION_UP event. 2918 if (!dispatched && !mIsExpanded && mIsGestureInProgress) { 2919 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev); 2920 } 2921 2922 mIsGestureInProgress = 2923 ev.getAction() != MotionEvent.ACTION_UP 2924 && ev.getAction() != MotionEvent.ACTION_CANCEL; 2925 2926 // If there is a deferred reorder action, and the gesture is over, run it now. 2927 if (mShouldReorderBubblesAfterGestureCompletes && !mIsGestureInProgress) { 2928 mShouldReorderBubblesAfterGestureCompletes = false; 2929 updateBubbleOrderInternal(mBubbleData.getBubbles(), false); 2930 } 2931 2932 return dispatched; 2933 } 2934 setFlyoutStateForDragLength(float deltaX)2935 void setFlyoutStateForDragLength(float deltaX) { 2936 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method 2937 // is continually called. 2938 if (mFlyout.getWidth() <= 0) { 2939 return; 2940 } 2941 2942 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 2943 mFlyoutDragDeltaX = deltaX; 2944 2945 final float collapsePercent = 2946 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth(); 2947 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent))); 2948 2949 // Calculate how to translate the flyout if it has been dragged too far in either direction. 2950 float overscrollTranslation = 0f; 2951 if (collapsePercent < 0f || collapsePercent > 1f) { 2952 // Whether we are more than 100% transitioned to the dot. 2953 final boolean overscrollingPastDot = collapsePercent > 1f; 2954 2955 // Whether we are overscrolling physically to the left - this can either be pulling the 2956 // flyout away from the stack (if the stack is on the right) or pushing it to the left 2957 // after it has already become the dot. 2958 final boolean overscrollingLeft = 2959 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f); 2960 overscrollTranslation = 2961 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1) 2962 * (overscrollingLeft ? -1 : 1) 2963 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR 2964 // Attenuate the smaller dot less than the larger flyout. 2965 / (overscrollingPastDot ? 2 : 1))); 2966 } 2967 2968 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation); 2969 } 2970 2971 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */ passEventToMagnetizedObject(MotionEvent event)2972 private boolean passEventToMagnetizedObject(MotionEvent event) { 2973 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); 2974 } 2975 dismissBubbleIfExists(@ullable BubbleViewProvider bubble)2976 private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) { 2977 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { 2978 if (mIsExpanded && mBubbleData.getBubbles().size() > 1 2979 && Objects.equals(bubble, mExpandedBubble)) { 2980 // If we have more than 1 bubble and it's the current bubble being dismissed, 2981 // we will perform the switch animation 2982 mIsBubbleSwitchAnimating = true; 2983 } 2984 mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE); 2985 } 2986 } 2987 2988 /** Prepares and starts the dismiss animation on the bubble stack. */ animateDismissBubble(View targetView, boolean applyAlpha)2989 private void animateDismissBubble(View targetView, boolean applyAlpha) { 2990 mViewBeingDismissed = targetView; 2991 2992 if (mViewBeingDismissed == null) { 2993 return; 2994 } 2995 if (applyAlpha) { 2996 mDismissBubbleAnimator.removeAllListeners(); 2997 mDismissBubbleAnimator.start(); 2998 } else { 2999 mDismissBubbleAnimator.removeAllListeners(); 3000 mDismissBubbleAnimator.addListener(new AnimatorListenerAdapter() { 3001 @Override 3002 public void onAnimationEnd(Animator animation) { 3003 super.onAnimationEnd(animation); 3004 resetDismissAnimator(); 3005 } 3006 3007 @Override 3008 public void onAnimationCancel(Animator animation) { 3009 super.onAnimationCancel(animation); 3010 resetDismissAnimator(); 3011 } 3012 }); 3013 mDismissBubbleAnimator.reverse(); 3014 } 3015 } 3016 resetDismissAnimator()3017 private void resetDismissAnimator() { 3018 mDismissBubbleAnimator.removeAllListeners(); 3019 mDismissBubbleAnimator.cancel(); 3020 3021 if (mViewBeingDismissed != null) { 3022 mViewBeingDismissed.setAlpha(1f); 3023 mViewBeingDismissed = null; 3024 } 3025 if (mDismissView != null) { 3026 mDismissView.getCircle().setScaleX(1f); 3027 mDismissView.getCircle().setScaleY(1f); 3028 } 3029 } 3030 3031 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */ animateFlyoutCollapsed(boolean collapsed, float velX)3032 private void animateFlyoutCollapsed(boolean collapsed, float velX) { 3033 final boolean onLeft = mStackAnimationController.isStackOnLeftSide(); 3034 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's 3035 // faster. 3036 mFlyoutTransitionSpring.getSpring().setStiffness( 3037 (mBubbleToExpandAfterFlyoutCollapse != null) 3038 ? SpringForce.STIFFNESS_MEDIUM 3039 : SpringForce.STIFFNESS_LOW); 3040 mFlyoutTransitionSpring 3041 .setStartValue(mFlyoutDragDeltaX) 3042 .setStartVelocity(velX) 3043 .animateToFinalPosition(collapsed 3044 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth()) 3045 : 0f); 3046 } 3047 shouldShowFlyout(Bubble bubble)3048 private boolean shouldShowFlyout(Bubble bubble) { 3049 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage(); 3050 final BadgedImageView bubbleView = bubble.getIconView(); 3051 if (flyoutMessage == null 3052 || flyoutMessage.message == null 3053 || !bubble.showFlyout() 3054 || isStackEduVisible() 3055 || isExpanded() 3056 || mIsExpansionAnimating 3057 || mIsGestureInProgress 3058 || mSensitiveNotificationProtectionActive 3059 || mBubbleToExpandAfterFlyoutCollapse != null 3060 || bubbleView == null) { 3061 if (bubbleView != null && mFlyout.getVisibility() != VISIBLE) { 3062 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 3063 } 3064 // Skip the message if none exists, we're expanded or animating expansion, or we're 3065 // about to expand a bubble from the previous tapped flyout, or if bubble view is null. 3066 return false; 3067 } 3068 return true; 3069 } 3070 3071 /** 3072 * Animates in the flyout for the given bubble, if available, and then hides it after some time. 3073 */ 3074 @VisibleForTesting animateInFlyoutForBubble(Bubble bubble)3075 void animateInFlyoutForBubble(Bubble bubble) { 3076 if (!shouldShowFlyout(bubble)) { 3077 return; 3078 } 3079 ProtoLog.d(WM_SHELL_BUBBLES, "animateFlyout=%s", bubble.getKey()); 3080 mFlyoutDragDeltaX = 0f; 3081 clearFlyoutOnHide(); 3082 mAfterFlyoutHidden = () -> { 3083 // Null it out to ensure it runs once. 3084 mAfterFlyoutHidden = null; 3085 3086 if (mBubbleToExpandAfterFlyoutCollapse != null) { 3087 // User tapped on the flyout and we should expand 3088 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse); 3089 mBubbleData.setExpanded(true); 3090 mBubbleToExpandAfterFlyoutCollapse = null; 3091 } 3092 3093 // Stop suppressing the dot now that the flyout has morphed into the dot. 3094 if (bubble.getIconView() != null) { 3095 bubble.getIconView().removeDotSuppressionFlag( 3096 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 3097 } 3098 // Hide the stack after a delay, if needed. 3099 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 3100 animateStashedState(true /* stashImmediately */); 3101 }; 3102 3103 // Suppress the dot when we are animating the flyout. 3104 bubble.getIconView().addDotSuppressionFlag( 3105 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE); 3106 3107 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0. 3108 post(() -> { 3109 // An auto-expanding bubble could have been posted during the time it takes to 3110 // layout. 3111 if (isExpanded() || bubble.getIconView() == null) { 3112 return; 3113 } 3114 final Runnable expandFlyoutAfterDelay = () -> { 3115 mAnimateInFlyout = () -> { 3116 mFlyout.setVisibility(VISIBLE); 3117 updateTemporarilyInvisibleAnimation(false /* hideImmediately */); 3118 mFlyoutDragDeltaX = 3119 mStackAnimationController.isStackOnLeftSide() 3120 ? -mFlyout.getWidth() 3121 : mFlyout.getWidth(); 3122 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */); 3123 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 3124 }; 3125 mFlyout.postDelayed(mAnimateInFlyout, 200); 3126 }; 3127 3128 3129 if (mFlyout.getVisibility() == View.VISIBLE) { 3130 mFlyout.animateUpdate(bubble.getFlyoutMessage(), 3131 mStackAnimationController.getStackPosition(), !bubble.showDot(), 3132 bubble.getIconView().getDotCenter(), 3133 mAfterFlyoutHidden /* onHide */); 3134 } else { 3135 mFlyout.setVisibility(INVISIBLE); 3136 mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), 3137 mStackAnimationController.getStackPosition(), 3138 mStackAnimationController.isStackOnLeftSide(), 3139 bubble.getIconView().getDotColor() /* dotColor */, 3140 expandFlyoutAfterDelay /* onLayoutComplete */, 3141 mAfterFlyoutHidden /* onHide */, 3142 bubble.getIconView().getDotCenter(), 3143 !bubble.showDot()); 3144 } 3145 mFlyout.bringToFront(); 3146 }); 3147 mFlyout.removeCallbacks(mHideFlyout); 3148 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER); 3149 logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT); 3150 } 3151 3152 /** Hide the flyout immediately and cancel any pending hide runnables. */ hideFlyoutImmediate()3153 private void hideFlyoutImmediate() { 3154 clearFlyoutOnHide(); 3155 mFlyout.removeCallbacks(mAnimateInFlyout); 3156 mFlyout.removeCallbacks(mHideFlyout); 3157 mFlyout.hideFlyout(); 3158 } 3159 clearFlyoutOnHide()3160 private void clearFlyoutOnHide() { 3161 mFlyout.removeCallbacks(mAnimateInFlyout); 3162 if (mAfterFlyoutHidden == null) { 3163 return; 3164 } 3165 mAfterFlyoutHidden.run(); 3166 mAfterFlyoutHidden = null; 3167 } 3168 3169 /** 3170 * Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager 3171 * to decide which touch events go to Bubbles. 3172 * 3173 * Bubbles is below the status bar/notification shade but above application windows. If you're 3174 * trying to get touch events from the status bar or another higher-level window layer, you'll 3175 * need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal 3176 * them. 3177 */ getTouchableRegion(Rect outRect)3178 public void getTouchableRegion(Rect outRect) { 3179 if (isStackEduVisible()) { 3180 // When user education shows then capture all touches 3181 outRect.set(0, 0, getWidth(), getHeight()); 3182 return; 3183 } 3184 3185 if (!mIsExpanded) { 3186 if (getBubbleCount() > 0 || mBubbleData.isShowingOverflow()) { 3187 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect); 3188 // Increase the touch target size of the bubble 3189 outRect.top -= mBubbleTouchPadding; 3190 outRect.left -= mBubbleTouchPadding; 3191 outRect.right += mBubbleTouchPadding; 3192 outRect.bottom += mBubbleTouchPadding; 3193 if (Flags.enableBubbleStashing()) { 3194 if (mStackOnLeftOrWillBe) { 3195 outRect.right += mBubbleTouchPadding; 3196 } else { 3197 outRect.left -= mBubbleTouchPadding; 3198 } 3199 } 3200 } 3201 } else { 3202 mBubbleContainer.getBoundsOnScreen(outRect); 3203 // Account for the IME in the touchable region so that the touchable region of the 3204 // Bubble window doesn't obscure the IME. The touchable region affects which areas 3205 // of the screen can be excluded by lower windows (IME is just above the embedded task) 3206 outRect.bottom -= mPositioner.getImeHeight(); 3207 } 3208 3209 if (mFlyout.getVisibility() == View.VISIBLE) { 3210 final Rect flyoutBounds = new Rect(); 3211 mFlyout.getBoundsOnScreen(flyoutBounds); 3212 outRect.union(flyoutBounds); 3213 } 3214 } 3215 requestUpdate()3216 private void requestUpdate() { 3217 if (mViewUpdatedRequested || mIsExpansionAnimating) { 3218 return; 3219 } 3220 mViewUpdatedRequested = true; 3221 getViewTreeObserver().addOnPreDrawListener(mViewUpdater); 3222 invalidate(); 3223 } 3224 3225 /** Hide or show the manage menu for the currently expanded bubble. */ 3226 @VisibleForTesting showManageMenu(boolean show)3227 public void showManageMenu(boolean show) { 3228 if ((mManageMenu.getVisibility() == VISIBLE) == show) return; 3229 ProtoLog.d(WM_SHELL_BUBBLES, "showManageMenu=%b for bubble=%s", 3230 show, (mExpandedBubble != null ? mExpandedBubble.getKey() : "null")); 3231 3232 mShowingManage = show; 3233 3234 // This should not happen, since the manage menu is only visible when there's an expanded 3235 // bubble. If we end up in this state, just hide the menu immediately. 3236 BubbleExpandedView expandedView = getExpandedView(); 3237 if (expandedView == null) { 3238 mManageMenu.setVisibility(View.INVISIBLE); 3239 mManageMenuScrim.setVisibility(INVISIBLE); 3240 mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */); 3241 return; 3242 } 3243 if (show) { 3244 mManageMenuScrim.setVisibility(VISIBLE); 3245 mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f); 3246 } 3247 Runnable endAction = () -> { 3248 if (!show) { 3249 mManageMenuScrim.setVisibility(INVISIBLE); 3250 mManageMenuScrim.setTranslationZ(0f); 3251 } 3252 }; 3253 3254 mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(show); 3255 mManageMenuScrim.animate() 3256 .setInterpolator(show ? ALPHA_IN : ALPHA_OUT) 3257 .alpha(show ? BUBBLE_EXPANDED_SCRIM_ALPHA : 0f) 3258 .withEndAction(endAction) 3259 .start(); 3260 3261 // If available, update the manage menu's settings option with the expanded bubble's app 3262 // name and icon. 3263 if (show) { 3264 final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); 3265 if (bubble != null && !bubble.isAppBubble()) { 3266 // Setup options for non app bubbles 3267 mManageDontBubbleView.setVisibility(VISIBLE); 3268 mManageSettingsIcon.setImageBitmap(bubble.getRawAppBadge()); 3269 mManageSettingsText.setText(getResources().getString( 3270 R.string.bubbles_app_settings, bubble.getAppName())); 3271 mManageSettingsView.setVisibility(VISIBLE); 3272 } else { 3273 // Setup options for app bubbles 3274 // App bubbles have no conversations 3275 // so we don't show the option to not bubble conversation 3276 mManageDontBubbleView.setVisibility(GONE); 3277 // App bubbles are not notification based 3278 // so we don't show the option to go to notification settings 3279 mManageSettingsView.setVisibility(GONE); 3280 } 3281 } 3282 3283 if (expandedView.getTaskView() != null) { 3284 expandedView.getTaskView().setObscuredTouchRect(mShowingManage 3285 ? new Rect(0, 0, getWidth(), getHeight()) 3286 : null); 3287 } 3288 3289 final boolean isLtr = 3290 getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_LTR; 3291 3292 // When the menu is open, it should be at these coordinates. The menu pops out to the right 3293 // in LTR and to the left in RTL. 3294 expandedView.getManageButtonBoundsOnScreen(mTempRect); 3295 final float margin = expandedView.getManageButtonMargin(); 3296 final float targetX = isLtr 3297 ? mTempRect.left - margin 3298 : mTempRect.right + margin - mManageMenu.getWidth(); 3299 final float menuHeight = getVisibleManageMenuHeight(); 3300 final float targetY = mTempRect.bottom - menuHeight; 3301 3302 final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; 3303 if (show) { 3304 mManageMenu.setScaleX(0.5f); 3305 mManageMenu.setScaleY(0.5f); 3306 mManageMenu.setTranslationX(targetX - xOffsetForAnimation); 3307 mManageMenu.setTranslationY(targetY + menuHeight / 4f); 3308 mManageMenu.setAlpha(0f); 3309 3310 PhysicsAnimator.getInstance(mManageMenu) 3311 .spring(DynamicAnimation.ALPHA, 1f) 3312 .spring(DynamicAnimation.SCALE_X, 1f) 3313 .spring(DynamicAnimation.SCALE_Y, 1f) 3314 .spring(DynamicAnimation.TRANSLATION_X, targetX) 3315 .spring(DynamicAnimation.TRANSLATION_Y, targetY) 3316 .withEndActions(() -> { 3317 View child = mManageMenu.getChildAt(0); 3318 child.requestAccessibilityFocus(); 3319 BubbleExpandedView expView = getExpandedView(); 3320 if (expView != null) { 3321 // Update the AV's obscured touchable region for the new state. 3322 expView.updateObscuredTouchableRegion(); 3323 } 3324 }) 3325 .start(); 3326 3327 mManageMenu.setVisibility(View.VISIBLE); 3328 } else { 3329 PhysicsAnimator.getInstance(mManageMenu) 3330 .spring(DynamicAnimation.ALPHA, 0f) 3331 .spring(DynamicAnimation.SCALE_X, 0.5f) 3332 .spring(DynamicAnimation.SCALE_Y, 0.5f) 3333 .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation) 3334 .spring(DynamicAnimation.TRANSLATION_Y, targetY + menuHeight / 4f) 3335 .withEndActions(() -> { 3336 mManageMenu.setVisibility(View.INVISIBLE); 3337 BubbleExpandedView expView = getExpandedView(); 3338 if (expView != null) { 3339 // Update the AV's obscured touchable region for the new state. 3340 expView.updateObscuredTouchableRegion(); 3341 } 3342 }) 3343 .start(); 3344 } 3345 } 3346 3347 /** 3348 * Checks whether manage menu don't bubble conversation action is available and visible 3349 * Used for testing 3350 */ 3351 @VisibleForTesting isManageMenuDontBubbleVisible()3352 public boolean isManageMenuDontBubbleVisible() { 3353 return mManageDontBubbleView != null && mManageDontBubbleView.getVisibility() == VISIBLE; 3354 } 3355 3356 /** 3357 * Checks whether manage menu notification settings action is available and visible 3358 * Used for testing 3359 */ 3360 @VisibleForTesting isManageMenuSettingsVisible()3361 public boolean isManageMenuSettingsVisible() { 3362 return mManageSettingsView != null && mManageSettingsView.getVisibility() == VISIBLE; 3363 } 3364 updateExpandedBubble()3365 private void updateExpandedBubble() { 3366 mExpandedViewContainer.removeAllViews(); 3367 BubbleExpandedView bev = getExpandedView(); 3368 if (mIsExpanded && bev != null) { 3369 bev.setContentVisibility(false); 3370 bev.setAnimating(!mIsExpansionAnimating); 3371 mExpandedViewContainerMatrix.setScaleX(0f); 3372 mExpandedViewContainerMatrix.setScaleY(0f); 3373 mExpandedViewContainerMatrix.setTranslate(0f, 0f); 3374 mExpandedViewContainer.setVisibility(View.INVISIBLE); 3375 mExpandedViewContainer.setAlpha(0f); 3376 mExpandedViewContainer.addView(bev); 3377 3378 postDelayed(() -> { 3379 // Set the Manage button click handler from postDelayed. This appears to resolve 3380 // a race condition with adding the BubbleExpandedView view to the expanded view 3381 // container. Due to the race condition the click handler sometimes is not set up 3382 // correctly and is never called. 3383 updateManageButtonListener(); 3384 }, 0); 3385 3386 if (!mIsExpansionAnimating) { 3387 mIsBubbleSwitchAnimating = true; 3388 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 3389 post(this::animateSwitchBubbles); 3390 }); 3391 } 3392 } 3393 } 3394 updateManageButtonListener()3395 private void updateManageButtonListener() { 3396 BubbleExpandedView bev = getExpandedView(); 3397 if (mIsExpanded && bev != null) { 3398 bev.setManageClickListener((view) -> { 3399 showManageMenu(true /* show */); 3400 }); 3401 } 3402 } 3403 3404 /** 3405 * Requests a snapshot from the currently expanded bubble's TaskView and displays it in a 3406 * SurfaceView. This allows us to load a newly expanded bubble's Activity into the TaskView, 3407 * while animating the (screenshot of the) previously selected bubble's content away. 3408 * 3409 * @param onComplete Callback to run once we're done here - called with 'false' if something 3410 * went wrong, or 'true' if the SurfaceView is now showing a screenshot of the 3411 * expanded bubble. 3412 */ screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete)3413 private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) { 3414 final BubbleExpandedView animatingOutExpandedView = getExpandedView(); 3415 if (!mIsExpanded || animatingOutExpandedView == null) { 3416 // You can't animate null. 3417 onComplete.accept(false); 3418 return; 3419 } 3420 3421 // Release the previous screenshot if it hasn't been released already. 3422 if (mAnimatingOutBubbleBuffer != null) { 3423 releaseAnimatingOutBubbleBuffer(); 3424 } 3425 3426 try { 3427 mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface(); 3428 } catch (Exception e) { 3429 // If we fail for any reason, print the stack trace and then notify the callback of our 3430 // failure. This is not expected to occur, but it's not worth crashing over. 3431 Log.wtf(TAG, e); 3432 onComplete.accept(false); 3433 } 3434 3435 if (mAnimatingOutBubbleBuffer == null 3436 || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null) { 3437 // While no exception was thrown, we were unable to get a snapshot. 3438 onComplete.accept(false); 3439 return; 3440 } 3441 3442 // Make sure the surface container's properties have been reset. 3443 PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel(); 3444 mAnimatingOutSurfaceContainer.setScaleX(1f); 3445 mAnimatingOutSurfaceContainer.setScaleY(1f); 3446 final float translationX = mPositioner.showBubblesVertically() && mStackOnLeftOrWillBe 3447 ? mExpandedViewContainer.getPaddingLeft() + mPositioner.getPointerSize() 3448 : mExpandedViewContainer.getPaddingLeft(); 3449 mAnimatingOutSurfaceContainer.setTranslationX(translationX); 3450 mAnimatingOutSurfaceContainer.setTranslationY(0); 3451 3452 final int[] taskViewLocation = animatingOutExpandedView.getTaskViewLocationOnScreen(); 3453 final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen(); 3454 3455 // Translate the surface to overlap the real TaskView. 3456 mAnimatingOutSurfaceContainer.setTranslationY( 3457 taskViewLocation[1] - surfaceViewLocation[1]); 3458 3459 // Set the width/height of the SurfaceView to match the snapshot. 3460 mAnimatingOutSurfaceView.getLayoutParams().width = 3461 mAnimatingOutBubbleBuffer.getHardwareBuffer().getWidth(); 3462 mAnimatingOutSurfaceView.getLayoutParams().height = 3463 mAnimatingOutBubbleBuffer.getHardwareBuffer().getHeight(); 3464 mAnimatingOutSurfaceView.requestLayout(); 3465 3466 // Post to wait for layout. 3467 post(() -> { 3468 // The buffer might have been destroyed if the user is mashing on bubbles, that's okay. 3469 if (mAnimatingOutBubbleBuffer == null 3470 || mAnimatingOutBubbleBuffer.getHardwareBuffer() == null 3471 || mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { 3472 onComplete.accept(false); 3473 return; 3474 } 3475 3476 if (!mIsExpanded || !mAnimatingOutSurfaceReady) { 3477 onComplete.accept(false); 3478 return; 3479 } 3480 3481 // Attach the buffer! We're now displaying the snapshot. 3482 mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace( 3483 mAnimatingOutBubbleBuffer.getHardwareBuffer(), 3484 mAnimatingOutBubbleBuffer.getColorSpace()); 3485 3486 mAnimatingOutSurfaceView.setAlpha(1f); 3487 mExpandedViewContainer.setVisibility(View.INVISIBLE); 3488 3489 mSurfaceSynchronizer.syncSurfaceAndRun(() -> { 3490 post(() -> { 3491 onComplete.accept(true); 3492 }); 3493 }); 3494 }); 3495 } 3496 3497 /** 3498 * Releases the buffer containing the screenshot of the animating-out bubble, if it exists and 3499 * isn't yet destroyed. 3500 */ releaseAnimatingOutBubbleBuffer()3501 private void releaseAnimatingOutBubbleBuffer() { 3502 if (mAnimatingOutBubbleBuffer != null 3503 && !mAnimatingOutBubbleBuffer.getHardwareBuffer().isClosed()) { 3504 mAnimatingOutBubbleBuffer.getHardwareBuffer().close(); 3505 } 3506 } 3507 updateExpandedView()3508 private void updateExpandedView() { 3509 boolean isOverflowExpanded = mExpandedBubble != null 3510 && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()); 3511 int[] paddings = mPositioner.getExpandedViewContainerPadding( 3512 mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded); 3513 mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]); 3514 BubbleExpandedView expandedView = getExpandedView(); 3515 if (expandedView != null) { 3516 PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), 3517 getState()); 3518 mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, 3519 mPositioner.showBubblesVertically() ? p.y : p.x)); 3520 mExpandedViewContainer.setTranslationX(0f); 3521 expandedView.updateTaskViewContentWidth(); 3522 expandedView.updateView(mExpandedViewContainer.getLocationOnScreen()); 3523 updatePointerPosition(false /* forIme */); 3524 } 3525 3526 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); 3527 } 3528 3529 /** 3530 * Updates whether each of the bubbles should show shadows. When collapsed & resting, only the 3531 * visible bubbles (top 2) will show a shadow. When the stack is being dragged, everything 3532 * shows a shadow. When an individual bubble is dragged out, it should show a shadow. 3533 * The bubble overflow is a special case and never has a shadow as it's ordered below the 3534 * rest of the bubbles and isn't visible unless the stack is expanded. 3535 * 3536 * @param isExpanded whether the stack will be expanded or not when the shadows are applied. 3537 */ updateBubbleShadows(boolean isExpanded)3538 private void updateBubbleShadows(boolean isExpanded) { 3539 final int childCount = mBubbleContainer.getChildCount(); 3540 for (int i = 0; i < childCount; i++) { 3541 final BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 3542 final boolean isOverflow = BubbleOverflow.KEY.equals(bv.getKey()); 3543 final boolean isDraggedOut = mMagnetizedObject != null 3544 && mMagnetizedObject.getUnderlyingObject().equals(bv); 3545 if (isDraggedOut) { 3546 // If it's dragged out, it's above all the other bubbles 3547 bv.setZ((mPositioner.getMaxBubbles() * mBubbleElevation) + 1); 3548 } else { 3549 bv.setZ(mPositioner.getZTranslation(i, isOverflow, isExpanded)); 3550 } 3551 } 3552 } 3553 3554 /** 3555 * When the bubbles are flung and then rest, the shadows stack up for the bubbles hidden 3556 * beneath the top two bubbles, to avoid this we animate the Z translations once the stack 3557 * is resting so that they fade away nicely. 3558 */ animateShadows()3559 private void animateShadows() { 3560 int bubbleCount = getBubbleCount(); 3561 for (int i = 0; i < bubbleCount; i++) { 3562 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 3563 boolean fullShadow = i < NUM_VISIBLE_WHEN_RESTING; 3564 if (!fullShadow) { 3565 bv.animate().translationZ(0).start(); 3566 } 3567 } 3568 } 3569 3570 private void updateBadges(boolean setBadgeForCollapsedStack) { 3571 int bubbleCount = getBubbleCount(); 3572 for (int i = 0; i < bubbleCount; i++) { 3573 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i); 3574 if (mIsExpanded) { 3575 // If we're not displaying vertically, we always show the badge on the left. 3576 boolean onLeft = mPositioner.showBubblesVertically() && !mStackOnLeftOrWillBe; 3577 bv.showDotAndBadge(onLeft); 3578 } else if (setBadgeForCollapsedStack) { 3579 if (i == 0) { 3580 bv.showDotAndBadge(!mStackOnLeftOrWillBe); 3581 } else { 3582 bv.hideDotAndBadge(!mStackOnLeftOrWillBe); 3583 } 3584 } 3585 } 3586 } 3587 3588 /** 3589 * Updates the position of the pointer based on the expanded bubble. 3590 * 3591 * @param forIme whether the position is being updated due to the ime appearing, in this case 3592 * the pointer is animated to the location. 3593 */ 3594 private void updatePointerPosition(boolean forIme) { 3595 BubbleExpandedView expandedView = getExpandedView(); 3596 if (mExpandedBubble == null || expandedView == null) { 3597 return; 3598 } 3599 int index = getBubbleIndex(mExpandedBubble); 3600 if (index == -1) { 3601 return; 3602 } 3603 PointF position = mPositioner.getExpandedBubbleXY(index, getState()); 3604 float bubblePosition = mPositioner.showBubblesVertically() 3605 ? position.y 3606 : position.x; 3607 expandedView.setPointerPosition(bubblePosition, 3608 mStackOnLeftOrWillBe, forIme /* animate */); 3609 } 3610 3611 /** 3612 * @return the number of bubbles in the stack view. 3613 */ 3614 public int getBubbleCount() { 3615 final int childCount = mBubbleContainer.getChildCount(); 3616 // Subtract 1 for the overflow button if it's showing. 3617 return mShowingOverflow ? childCount - 1 : childCount; 3618 } 3619 3620 /** 3621 * Finds the bubble index within the stack. 3622 * 3623 * @param provider the bubble view provider with the bubble to look up. 3624 * @return the index of the bubble view within the bubble stack. The range of the position 3625 * is between 0 and the bubble count minus 1. 3626 */ 3627 int getBubbleIndex(@Nullable BubbleViewProvider provider) { 3628 if (provider == null) { 3629 return -1; 3630 } 3631 return mBubbleContainer.indexOfChild(provider.getIconView()); 3632 } 3633 3634 /** 3635 * Menu height calculated for animation 3636 * It takes into account view visibility to get the correct total height 3637 */ 3638 private float getVisibleManageMenuHeight() { 3639 float menuHeight = 0; 3640 3641 for (int i = 0; i < mManageMenu.getChildCount(); i++) { 3642 View subview = mManageMenu.getChildAt(i); 3643 3644 if (subview.getVisibility() == VISIBLE) { 3645 menuHeight += subview.getHeight(); 3646 } 3647 } 3648 3649 return menuHeight; 3650 } 3651 3652 /** 3653 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places. 3654 */ 3655 public float getNormalizedXPosition() { 3656 int width = mPositioner.getAvailableRect().width(); 3657 float stackPosition = width > 0 ? getStackPosition().x / width : 0; 3658 return new BigDecimal(stackPosition) 3659 .setScale(4, RoundingMode.CEILING.HALF_UP) 3660 .floatValue(); 3661 } 3662 3663 /** 3664 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places. 3665 */ 3666 public float getNormalizedYPosition() { 3667 int height = mPositioner.getAvailableRect().height(); 3668 float stackPosition = height > 0 ? getStackPosition().y / height : 0; 3669 return new BigDecimal(stackPosition) 3670 .setScale(4, RoundingMode.CEILING.HALF_UP) 3671 .floatValue(); 3672 } 3673 3674 /** @return the position of the bubble stack. */ 3675 public PointF getStackPosition() { 3676 return mStackAnimationController.getStackPosition(); 3677 } 3678 3679 /** 3680 * Logs the bubble UI event. 3681 * 3682 * @param provider the bubble view provider that is being interacted on. Null value indicates 3683 * that the user interaction is not specific to one bubble. 3684 * @param action the user interaction enum. 3685 */ 3686 private void logBubbleEvent(@Nullable BubbleViewProvider provider, int action) { 3687 final String packageName = 3688 (provider != null && provider instanceof Bubble) 3689 ? ((Bubble) provider).getPackageName() 3690 : "null"; 3691 mBubbleData.logBubbleEvent(provider, 3692 action, 3693 packageName, 3694 getBubbleCount(), 3695 getBubbleIndex(provider), 3696 getNormalizedXPosition(), 3697 getNormalizedYPosition()); 3698 } 3699 3700 /** For debugging only */ 3701 List<Bubble> getBubblesOnScreen() { 3702 List<Bubble> bubbles = new ArrayList<>(); 3703 for (int i = 0; i < getBubbleCount(); i++) { 3704 View child = mBubbleContainer.getChildAt(i); 3705 if (child instanceof BadgedImageView) { 3706 String key = ((BadgedImageView) child).getKey(); 3707 Bubble bubble = mBubbleData.getBubbleInStackWithKey(key); 3708 bubbles.add(bubble); 3709 } 3710 } 3711 return bubbles; 3712 } 3713 3714 /** @return the current stack state. */ 3715 public StackViewState getState() { 3716 mStackViewState.numberOfBubbles = mBubbleContainer.getChildCount(); 3717 mStackViewState.selectedIndex = getBubbleIndex(mExpandedBubble); 3718 mStackViewState.onLeft = mStackOnLeftOrWillBe; 3719 return mStackViewState; 3720 } 3721 3722 /** 3723 * Handles vertical offset changes, e.g. when one handed mode is switched on/off. 3724 * 3725 * @param offset new vertical offset. 3726 */ 3727 void onVerticalOffsetChanged(int offset) { 3728 // adjust dismiss view vertical position, so that it is still visible to the user 3729 ViewGroup.LayoutParams lp = mDismissView.getLayoutParams(); 3730 if (lp instanceof FrameLayout.LayoutParams) { 3731 FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) lp; 3732 layoutParams.bottomMargin = offset; 3733 mDismissView.setLayoutParams(layoutParams); 3734 } 3735 mMagneticTarget.setScreenVerticalOffset(offset); 3736 mMagneticTarget.updateLocationOnScreen(); 3737 } 3738 3739 /** 3740 * Removes the overflow view from the stack. This allows for re-adding it later to a new stack. 3741 */ 3742 void resetOverflowView() { 3743 BadgedImageView overflowIcon = mBubbleOverflow.getIconView(); 3744 if (overflowIcon != null) { 3745 PhysicsAnimationLayout parent = (PhysicsAnimationLayout) overflowIcon.getParent(); 3746 if (parent != null) { 3747 parent.removeViewNoAnimation(overflowIcon); 3748 } 3749 } 3750 } 3751 3752 /** 3753 * Holds some commonly queried information about the stack. 3754 */ 3755 public static class StackViewState { 3756 // Number of bubbles (including the overflow itself) in the stack. 3757 public int numberOfBubbles; 3758 // The selected index if the stack is expanded. 3759 public int selectedIndex; 3760 // Whether the stack is resting on the left or right side of the screen when collapsed. 3761 public boolean onLeft; 3762 } 3763 3764 /** 3765 * Representation of stack position that uses relative properties rather than absolute 3766 * coordinates. This is used to maintain similar stack positions across configuration changes. 3767 */ 3768 public static class RelativeStackPosition { 3769 /** Whether to place the stack at the leftmost allowed position. */ 3770 private boolean mOnLeft; 3771 3772 /** 3773 * How far down the vertically allowed region to place the stack. For example, if the stack 3774 * allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at 3775 * 100 + (0.2f * 1000) = 300. 3776 */ 3777 private float mVerticalOffsetPercent; 3778 3779 public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) { 3780 mOnLeft = onLeft; 3781 mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent); 3782 } 3783 3784 /** Constructs a relative position given a region and a point in that region. */ 3785 public RelativeStackPosition(PointF position, RectF region) { 3786 mOnLeft = position.x < region.width() / 2; 3787 mVerticalOffsetPercent = 3788 clampVerticalOffsetPercent((position.y - region.top) / region.height()); 3789 } 3790 3791 /** Ensures that the offset percent is between 0f and 1f. */ 3792 private float clampVerticalOffsetPercent(float offsetPercent) { 3793 return Math.max(0f, Math.min(1f, offsetPercent)); 3794 } 3795 3796 /** 3797 * Given an allowable stack position region, returns the point within that region 3798 * represented by this relative position. 3799 */ 3800 public PointF getAbsolutePositionInRegion(RectF region) { 3801 return new PointF( 3802 mOnLeft ? region.left : region.right, 3803 region.top + mVerticalOffsetPercent * region.height()); 3804 } 3805 } 3806 } 3807