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