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