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