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