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