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