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