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