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