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