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.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
23 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
24 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.ValueAnimator;
29 import android.annotation.NonNull;
30 import android.app.Notification;
31 import android.content.Context;
32 import android.content.res.Configuration;
33 import android.content.res.Resources;
34 import android.graphics.ColorMatrix;
35 import android.graphics.ColorMatrixColorFilter;
36 import android.graphics.Paint;
37 import android.graphics.Point;
38 import android.graphics.PointF;
39 import android.graphics.Rect;
40 import android.graphics.RectF;
41 import android.os.Bundle;
42 import android.os.VibrationEffect;
43 import android.os.Vibrator;
44 import android.service.notification.StatusBarNotification;
45 import android.util.Log;
46 import android.util.StatsLog;
47 import android.view.Choreographer;
48 import android.view.DisplayCutout;
49 import android.view.Gravity;
50 import android.view.LayoutInflater;
51 import android.view.MotionEvent;
52 import android.view.View;
53 import android.view.ViewTreeObserver;
54 import android.view.WindowInsets;
55 import android.view.WindowManager;
56 import android.view.accessibility.AccessibilityNodeInfo;
57 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
58 import android.view.animation.AccelerateDecelerateInterpolator;
59 import android.widget.FrameLayout;
60 
61 import androidx.annotation.MainThread;
62 import androidx.annotation.Nullable;
63 import androidx.dynamicanimation.animation.DynamicAnimation;
64 import androidx.dynamicanimation.animation.FloatPropertyCompat;
65 import androidx.dynamicanimation.animation.SpringAnimation;
66 import androidx.dynamicanimation.animation.SpringForce;
67 
68 import com.android.internal.annotations.VisibleForTesting;
69 import com.android.internal.widget.ViewClippingUtil;
70 import com.android.systemui.R;
71 import com.android.systemui.bubbles.animation.ExpandedAnimationController;
72 import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
73 import com.android.systemui.bubbles.animation.StackAnimationController;
74 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
75 
76 import java.io.FileDescriptor;
77 import java.io.PrintWriter;
78 import java.math.BigDecimal;
79 import java.math.RoundingMode;
80 import java.util.ArrayList;
81 import java.util.Collections;
82 import java.util.List;
83 
84 /**
85  * Renders bubbles in a stack and handles animating expanded and collapsed states.
86  */
87 public class BubbleStackView extends FrameLayout {
88     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
89 
90     /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
91     static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
92 
93     /** Velocity required to dismiss the flyout via drag. */
94     private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
95 
96     /**
97      * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
98      * for every 8 pixels overscrolled).
99      */
100     private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
101 
102     /** Duration of the flyout alpha animations. */
103     private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
104 
105     /** Percent to darken the bubbles when they're in the dismiss target. */
106     private static final float DARKEN_PERCENT = 0.3f;
107 
108     /** How long to wait, in milliseconds, before hiding the flyout. */
109     @VisibleForTesting
110     static final int FLYOUT_HIDE_AFTER = 5000;
111 
112     /**
113      * Interface to synchronize {@link View} state and the screen.
114      *
115      * {@hide}
116      */
117     interface SurfaceSynchronizer {
118         /**
119          * Wait until requested change on a {@link View} is reflected on the screen.
120          *
121          * @param callback callback to run after the change is reflected on the screen.
122          */
syncSurfaceAndRun(Runnable callback)123         void syncSurfaceAndRun(Runnable callback);
124     }
125 
126     private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
127             new SurfaceSynchronizer() {
128         @Override
129         public void syncSurfaceAndRun(Runnable callback) {
130             Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
131                 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
132                 // the requested change is reflected on the screen.
133                 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
134                 // surfaces, rewrite this logic with them.
135                 private int mFrameWait = 2;
136 
137                 @Override
138                 public void doFrame(long frameTimeNanos) {
139                     if (--mFrameWait > 0) {
140                         Choreographer.getInstance().postFrameCallback(this);
141                     } else {
142                         callback.run();
143                     }
144                 }
145             });
146         }
147     };
148 
149     private Point mDisplaySize;
150 
151     private final SpringAnimation mExpandedViewXAnim;
152     private final SpringAnimation mExpandedViewYAnim;
153     private final BubbleData mBubbleData;
154 
155     private final Vibrator mVibrator;
156     private final ValueAnimator mDesaturateAndDarkenAnimator;
157     private final Paint mDesaturateAndDarkenPaint = new Paint();
158 
159     private PhysicsAnimationLayout mBubbleContainer;
160     private StackAnimationController mStackAnimationController;
161     private ExpandedAnimationController mExpandedAnimationController;
162 
163     private FrameLayout mExpandedViewContainer;
164 
165     private BubbleFlyoutView mFlyout;
166     /** Runnable that fades out the flyout and then sets it to GONE. */
167     private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
168     /**
169      * Callback to run after the flyout hides. Also called if a new flyout is shown before the
170      * previous one animates out.
171      */
172     private Runnable mFlyoutOnHide;
173 
174     /** Layout change listener that moves the stack to the nearest valid position on rotation. */
175     private OnLayoutChangeListener mOrientationChangedListener;
176     /** Whether the stack was on the left side of the screen prior to rotation. */
177     private boolean mWasOnLeftBeforeRotation = false;
178     /**
179      * How far down the screen the stack was before rotation, in terms of percentage of the way down
180      * the allowable region. Defaults to -1 if not set.
181      */
182     private float mVerticalPosPercentBeforeRotation = -1;
183 
184     private int mBubbleSize;
185     private int mBubblePaddingTop;
186     private int mBubbleTouchPadding;
187     private int mExpandedViewPadding;
188     private int mExpandedAnimateXDistance;
189     private int mExpandedAnimateYDistance;
190     private int mPointerHeight;
191     private int mStatusBarHeight;
192     private int mImeOffset;
193     private BubbleIconFactory mBubbleIconFactory;
194     private Bubble mExpandedBubble;
195     private boolean mIsExpanded;
196 
197     /** Whether the stack is currently on the left side of the screen, or animating there. */
198     private boolean mStackOnLeftOrWillBe = false;
199 
200     /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
201     private boolean mIsGestureInProgress = false;
202 
203     /** Description of current animation controller state. */
dump(FileDescriptor fd, PrintWriter pw, String[] args)204     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
205         pw.println("Stack view state:");
206         pw.print("  gestureInProgress:    "); pw.println(mIsGestureInProgress);
207         pw.print("  showingDismiss:       "); pw.println(mShowingDismiss);
208         pw.print("  isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
209         pw.print("  draggingInDismiss:    "); pw.println(mDraggingInDismissTarget);
210         pw.print("  animatingMagnet:      "); pw.println(mAnimatingMagnet);
211         mStackAnimationController.dump(fd, pw, args);
212         mExpandedAnimationController.dump(fd, pw, args);
213     }
214 
215     private BubbleTouchHandler mTouchHandler;
216     private BubbleController.BubbleExpandListener mExpandListener;
217 
218     private boolean mViewUpdatedRequested = false;
219     private boolean mIsExpansionAnimating = false;
220     private boolean mShowingDismiss = false;
221 
222     /**
223      * Whether the user is currently dragging their finger within the dismiss target. In this state
224      * the stack will be magnetized to the center of the target, so we shouldn't move it until the
225      * touch exits the dismiss target area.
226      */
227     private boolean mDraggingInDismissTarget = false;
228 
229     /** Whether the stack is magneting towards the dismiss target. */
230     private boolean mAnimatingMagnet = false;
231 
232     /** The view to desaturate/darken when magneted to the dismiss target. */
233     private View mDesaturateAndDarkenTargetView;
234 
235     private LayoutInflater mInflater;
236 
237     // Used for determining view / touch intersection
238     int[] mTempLoc = new int[2];
239     RectF mTempRect = new RectF();
240 
241     private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
242 
243     private ViewTreeObserver.OnPreDrawListener mViewUpdater =
244             new ViewTreeObserver.OnPreDrawListener() {
245                 @Override
246                 public boolean onPreDraw() {
247                     getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
248                     updateExpandedView();
249                     mViewUpdatedRequested = false;
250                     return true;
251                 }
252             };
253 
254     private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
255             this::updateSystemGestureExcludeRects;
256 
257     private ViewClippingUtil.ClippingParameters mClippingParameters =
258             new ViewClippingUtil.ClippingParameters() {
259 
260                 @Override
261                 public boolean shouldFinish(View view) {
262                     return false;
263                 }
264 
265                 @Override
266                 public boolean isClippingEnablingAllowed(View view) {
267                     return !mIsExpanded;
268                 }
269             };
270 
271     /** Float property that 'drags' the flyout. */
272     private final FloatPropertyCompat mFlyoutCollapseProperty =
273             new FloatPropertyCompat("FlyoutCollapseSpring") {
274                 @Override
275                 public float getValue(Object o) {
276                     return mFlyoutDragDeltaX;
277                 }
278 
279                 @Override
280                 public void setValue(Object o, float v) {
281                     onFlyoutDragged(v);
282                 }
283             };
284 
285     /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
286     private final SpringAnimation mFlyoutTransitionSpring =
287             new SpringAnimation(this, mFlyoutCollapseProperty);
288 
289     /** Distance the flyout has been dragged in the X axis. */
290     private float mFlyoutDragDeltaX = 0f;
291 
292     /**
293      * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
294      */
295     private Runnable mAnimateInFlyout;
296 
297     /**
298      * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
299      * it immediately.
300      */
301     private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
302             (dynamicAnimation, b, v, v1) -> {
303                 if (mFlyoutDragDeltaX == 0) {
304                     mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
305                 } else {
306                     mFlyout.hideFlyout();
307                 }
308             };
309 
310     @NonNull
311     private final SurfaceSynchronizer mSurfaceSynchronizer;
312 
313     private BubbleDismissView mDismissContainer;
314     private Runnable mAfterMagnet;
315 
316     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
317 
BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer)318     public BubbleStackView(Context context, BubbleData data,
319             @Nullable SurfaceSynchronizer synchronizer) {
320         super(context);
321 
322         mBubbleData = data;
323         mInflater = LayoutInflater.from(context);
324         mTouchHandler = new BubbleTouchHandler(this, data, context);
325         setOnTouchListener(mTouchHandler);
326         mInflater = LayoutInflater.from(context);
327 
328         Resources res = getResources();
329         mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
330         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
331         mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
332         mExpandedAnimateXDistance =
333                 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
334         mExpandedAnimateYDistance =
335                 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
336         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
337 
338         mStatusBarHeight =
339                 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
340         mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
341 
342         mDisplaySize = new Point();
343         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
344         // We use the real size & subtract screen decorations / window insets ourselves when needed
345         wm.getDefaultDisplay().getRealSize(mDisplaySize);
346 
347         mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
348 
349         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
350         int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
351 
352         mStackAnimationController = new StackAnimationController();
353 
354         mExpandedAnimationController = new ExpandedAnimationController(
355                 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
356         mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
357 
358         mBubbleContainer = new PhysicsAnimationLayout(context);
359         mBubbleContainer.setActiveController(mStackAnimationController);
360         mBubbleContainer.setElevation(elevation);
361         mBubbleContainer.setClipChildren(false);
362         addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
363 
364         mBubbleIconFactory = new BubbleIconFactory(context);
365 
366         mExpandedViewContainer = new FrameLayout(context);
367         mExpandedViewContainer.setElevation(elevation);
368         mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
369                 mExpandedViewPadding, mExpandedViewPadding);
370         mExpandedViewContainer.setClipChildren(false);
371         addView(mExpandedViewContainer);
372 
373         setUpFlyout();
374         mFlyoutTransitionSpring.setSpring(new SpringForce()
375                 .setStiffness(SpringForce.STIFFNESS_LOW)
376                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
377         mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
378 
379         mDismissContainer = new BubbleDismissView(mContext);
380         mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams(
381                 MATCH_PARENT,
382                 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
383                 Gravity.BOTTOM));
384         addView(mDismissContainer);
385 
386         mExpandedViewXAnim =
387                 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
388         mExpandedViewXAnim.setSpring(
389                 new SpringForce()
390                         .setStiffness(SpringForce.STIFFNESS_LOW)
391                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
392 
393         mExpandedViewYAnim =
394                 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
395         mExpandedViewYAnim.setSpring(
396                 new SpringForce()
397                         .setStiffness(SpringForce.STIFFNESS_LOW)
398                         .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
399         mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
400             if (mIsExpanded && mExpandedBubble != null) {
401                 mExpandedBubble.getExpandedView().updateView();
402             }
403         });
404 
405         setClipChildren(false);
406         setFocusable(true);
407         mBubbleContainer.bringToFront();
408 
409         setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
410             if (!mIsExpanded || mIsExpansionAnimating) {
411                 return view.onApplyWindowInsets(insets);
412             }
413             mExpandedAnimationController.updateYPosition(
414                     // Update the insets after we're done translating otherwise position
415                     // calculation for them won't be correct.
416                     () -> mExpandedBubble.getExpandedView().updateInsets(insets));
417             return view.onApplyWindowInsets(insets);
418         });
419 
420         mOrientationChangedListener =
421                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
422                     mExpandedAnimationController.updateOrientation(mOrientation, mDisplaySize);
423                     mStackAnimationController.updateOrientation(mOrientation);
424 
425                     // Reposition & adjust the height for new orientation
426                     if (mIsExpanded) {
427                         mExpandedViewContainer.setTranslationY(getExpandedViewY());
428                         mExpandedBubble.getExpandedView().updateView();
429                     }
430 
431                     // Need to update the padding around the view
432                     WindowInsets insets = getRootWindowInsets();
433                     int leftPadding = mExpandedViewPadding;
434                     int rightPadding = mExpandedViewPadding;
435                     if (insets != null) {
436                         // Can't have the expanded view overlaying notches
437                         int cutoutLeft = 0;
438                         int cutoutRight = 0;
439                         DisplayCutout cutout = insets.getDisplayCutout();
440                         if (cutout != null) {
441                             cutoutLeft = cutout.getSafeInsetLeft();
442                             cutoutRight = cutout.getSafeInsetRight();
443                         }
444                         // Or overlaying nav or status bar
445                         leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
446                         rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
447                     }
448                     mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
449                             rightPadding, mExpandedViewPadding);
450 
451                     if (mIsExpanded) {
452                         // Re-draw bubble row and pointer for new orientation.
453                         mExpandedAnimationController.expandFromStack(() -> {
454                             updatePointerPosition();
455                         } /* after */);
456                     }
457                     if (mVerticalPosPercentBeforeRotation >= 0) {
458                         mStackAnimationController.moveStackToSimilarPositionAfterRotation(
459                                 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
460                     }
461                     removeOnLayoutChangeListener(mOrientationChangedListener);
462                 };
463 
464         // This must be a separate OnDrawListener since it should be called for every draw.
465         getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
466 
467         final ColorMatrix animatedMatrix = new ColorMatrix();
468         final ColorMatrix darkenMatrix = new ColorMatrix();
469 
470         mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
471         mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
472             final float animatedValue = (float) animation.getAnimatedValue();
473             animatedMatrix.setSaturation(animatedValue);
474 
475             final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
476             darkenMatrix.setScale(
477                     1f - animatedDarkenValue /* red */,
478                     1f - animatedDarkenValue /* green */,
479                     1f - animatedDarkenValue /* blue */,
480                     1f /* alpha */);
481 
482             // Concat the matrices so that the animatedMatrix both desaturates and darkens.
483             animatedMatrix.postConcat(darkenMatrix);
484 
485             // Update the paint and apply it to the bubble container.
486             mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
487             mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
488         });
489     }
490 
setUpFlyout()491     private void setUpFlyout() {
492         if (mFlyout != null) {
493             removeView(mFlyout);
494         }
495         mFlyout = new BubbleFlyoutView(getContext());
496         mFlyout.setVisibility(GONE);
497         mFlyout.animate()
498                 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
499                 .setInterpolator(new AccelerateDecelerateInterpolator());
500         addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
501     }
502 
503     /**
504      * Handle theme changes.
505      */
onThemeChanged()506     public void onThemeChanged() {
507         // Recreate icon factory to update default adaptive icon scale.
508         mBubbleIconFactory = new BubbleIconFactory(mContext);
509         setUpFlyout();
510         for (Bubble b: mBubbleData.getBubbles()) {
511             b.getIconView().setBubbleIconFactory(mBubbleIconFactory);
512             b.getIconView().updateViews();
513             b.getExpandedView().applyThemeAttrs();
514         }
515     }
516 
517     /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
onOrientationChanged(int orientation)518     public void onOrientationChanged(int orientation) {
519         mOrientation = orientation;
520 
521         // Display size is based on the rotation device was in when requested, we should update it
522         // We use the real size & subtract screen decorations / window insets ourselves when needed
523         WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
524         wm.getDefaultDisplay().getRealSize(mDisplaySize);
525 
526         // Some resources change depending on orientation
527         Resources res = getContext().getResources();
528         mStatusBarHeight = res.getDimensionPixelSize(
529                 com.android.internal.R.dimen.status_bar_height);
530         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
531 
532         final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
533         mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
534         mVerticalPosPercentBeforeRotation =
535                 (mStackAnimationController.getStackPosition().y - allowablePos.top)
536                         / (allowablePos.bottom - allowablePos.top);
537         addOnLayoutChangeListener(mOrientationChangedListener);
538         hideFlyoutImmediate();
539     }
540 
541     @Override
getBoundsOnScreen(Rect outRect, boolean clipToParent)542     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
543         getBoundsOnScreen(outRect);
544     }
545 
546     @Override
onDetachedFromWindow()547     protected void onDetachedFromWindow() {
548         super.onDetachedFromWindow();
549         getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
550     }
551 
552     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)553     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
554         super.onInitializeAccessibilityNodeInfoInternal(info);
555 
556         // Custom actions.
557         AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
558                 getContext().getResources()
559                         .getString(R.string.bubble_accessibility_action_move_top_left));
560         info.addAction(moveTopLeft);
561 
562         AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
563                 getContext().getResources()
564                         .getString(R.string.bubble_accessibility_action_move_top_right));
565         info.addAction(moveTopRight);
566 
567         AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
568                 getContext().getResources()
569                         .getString(R.string.bubble_accessibility_action_move_bottom_left));
570         info.addAction(moveBottomLeft);
571 
572         AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
573                 getContext().getResources()
574                         .getString(R.string.bubble_accessibility_action_move_bottom_right));
575         info.addAction(moveBottomRight);
576 
577         // Default actions.
578         info.addAction(AccessibilityAction.ACTION_DISMISS);
579         if (mIsExpanded) {
580             info.addAction(AccessibilityAction.ACTION_COLLAPSE);
581         } else {
582             info.addAction(AccessibilityAction.ACTION_EXPAND);
583         }
584     }
585 
586     @Override
performAccessibilityActionInternal(int action, Bundle arguments)587     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
588         if (super.performAccessibilityActionInternal(action, arguments)) {
589             return true;
590         }
591         final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
592 
593         // R constants are not final so we cannot use switch-case here.
594         if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
595             mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
596             return true;
597         } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
598             mBubbleData.setExpanded(false);
599             return true;
600         } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
601             mBubbleData.setExpanded(true);
602             return true;
603         } else if (action == R.id.action_move_top_left) {
604             mStackAnimationController.springStack(stackBounds.left, stackBounds.top);
605             return true;
606         } else if (action == R.id.action_move_top_right) {
607             mStackAnimationController.springStack(stackBounds.right, stackBounds.top);
608             return true;
609         } else if (action == R.id.action_move_bottom_left) {
610             mStackAnimationController.springStack(stackBounds.left, stackBounds.bottom);
611             return true;
612         } else if (action == R.id.action_move_bottom_right) {
613             mStackAnimationController.springStack(stackBounds.right, stackBounds.bottom);
614             return true;
615         }
616         return false;
617     }
618 
619     /**
620      * Update content description for a11y TalkBack.
621      */
updateContentDescription()622     public void updateContentDescription() {
623         if (mBubbleData.getBubbles().isEmpty()) {
624             return;
625         }
626         Bubble topBubble = mBubbleData.getBubbles().get(0);
627         String appName = topBubble.getAppName();
628         Notification notification = topBubble.getEntry().notification.getNotification();
629         CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
630         String titleStr = getResources().getString(R.string.stream_notification);
631         if (titleCharSeq != null) {
632             titleStr = titleCharSeq.toString();
633         }
634         int moreCount = mBubbleContainer.getChildCount() - 1;
635 
636         // Example: Title from app name.
637         String singleDescription = getResources().getString(
638                 R.string.bubble_content_description_single, titleStr, appName);
639 
640         // Example: Title from app name and 4 more.
641         String stackDescription = getResources().getString(
642                 R.string.bubble_content_description_stack, titleStr, appName, moreCount);
643 
644         if (mIsExpanded) {
645             // TODO(b/129522932) - update content description for each bubble in expanded view.
646         } else {
647             // Collapsed stack.
648             if (moreCount > 0) {
649                 mBubbleContainer.setContentDescription(stackDescription);
650             } else {
651                 mBubbleContainer.setContentDescription(singleDescription);
652             }
653         }
654     }
655 
updateSystemGestureExcludeRects()656     private void updateSystemGestureExcludeRects() {
657         // Exclude the region occupied by the first BubbleView in the stack
658         Rect excludeZone = mSystemGestureExclusionRects.get(0);
659         if (mBubbleContainer.getChildCount() > 0) {
660             View firstBubble = mBubbleContainer.getChildAt(0);
661             excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
662                     firstBubble.getBottom());
663             excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
664                     (int) (firstBubble.getTranslationY() + 0.5f));
665             mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
666         } else {
667             excludeZone.setEmpty();
668             mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
669         }
670     }
671 
672     /**
673      * Updates the visibility of the 'dot' indicating an update on the bubble.
674      *
675      * @param key the {@link NotificationEntry#key} associated with the bubble.
676      */
updateDotVisibility(String key)677     public void updateDotVisibility(String key) {
678         Bubble b = mBubbleData.getBubbleWithKey(key);
679         if (b != null) {
680             b.updateDotVisibility();
681         }
682     }
683 
684     /**
685      * Sets the listener to notify when the bubble stack is expanded.
686      */
setExpandListener(BubbleController.BubbleExpandListener listener)687     public void setExpandListener(BubbleController.BubbleExpandListener listener) {
688         mExpandListener = listener;
689     }
690 
691     /**
692      * Whether the stack of bubbles is expanded or not.
693      */
isExpanded()694     public boolean isExpanded() {
695         return mIsExpanded;
696     }
697 
698     /**
699      * Whether the stack of bubbles is animating to or from expansion.
700      */
isExpansionAnimating()701     public boolean isExpansionAnimating() {
702         return mIsExpansionAnimating;
703     }
704 
705     /**
706      * The {@link BubbleView} that is expanded, null if one does not exist.
707      */
getExpandedBubbleView()708     BubbleView getExpandedBubbleView() {
709         return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
710     }
711 
712     /**
713      * The {@link Bubble} that is expanded, null if one does not exist.
714      */
getExpandedBubble()715     Bubble getExpandedBubble() {
716         return mExpandedBubble;
717     }
718 
719     /**
720      * Sets the bubble that should be expanded and expands if needed.
721      *
722      * @param key the {@link NotificationEntry#key} associated with the bubble to expand.
723      * @deprecated replaced by setSelectedBubble(Bubble) + setExpanded(true)
724      */
725     @Deprecated
setExpandedBubble(String key)726     void setExpandedBubble(String key) {
727         Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key);
728         if (bubbleToExpand != null) {
729             setSelectedBubble(bubbleToExpand);
730             bubbleToExpand.setShowInShadeWhenBubble(false);
731             setExpanded(true);
732         }
733     }
734 
735     // via BubbleData.Listener
addBubble(Bubble bubble)736     void addBubble(Bubble bubble) {
737         if (DEBUG_BUBBLE_STACK_VIEW) {
738             Log.d(TAG, "addBubble: " + bubble);
739         }
740 
741         if (mBubbleContainer.getChildCount() == 0) {
742             mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
743         }
744 
745         bubble.inflate(mInflater, this);
746         bubble.getIconView().setBubbleIconFactory(mBubbleIconFactory);
747         bubble.getIconView().updateViews();
748 
749         // Set the dot position to the opposite of the side the stack is resting on, since the stack
750         // resting slightly off-screen would result in the dot also being off-screen.
751         bubble.getIconView().setDotPosition(
752                 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
753 
754         mBubbleContainer.addView(bubble.getIconView(), 0,
755                 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
756         ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
757         animateInFlyoutForBubble(bubble);
758         requestUpdate();
759         logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
760         updatePointerPosition();
761     }
762 
763     // via BubbleData.Listener
removeBubble(Bubble bubble)764     void removeBubble(Bubble bubble) {
765         if (DEBUG_BUBBLE_STACK_VIEW) {
766             Log.d(TAG, "removeBubble: " + bubble);
767         }
768         // Remove it from the views
769         int removedIndex = mBubbleContainer.indexOfChild(bubble.getIconView());
770         if (removedIndex >= 0) {
771             mBubbleContainer.removeViewAt(removedIndex);
772             bubble.cleanupExpandedState();
773             logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
774         } else {
775             Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
776         }
777         updatePointerPosition();
778     }
779 
780     // via BubbleData.Listener
updateBubble(Bubble bubble)781     void updateBubble(Bubble bubble) {
782         animateInFlyoutForBubble(bubble);
783         requestUpdate();
784         logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
785     }
786 
updateBubbleOrder(List<Bubble> bubbles)787     public void updateBubbleOrder(List<Bubble> bubbles) {
788         for (int i = 0; i < bubbles.size(); i++) {
789             Bubble bubble = bubbles.get(i);
790             mBubbleContainer.reorderView(bubble.getIconView(), i);
791         }
792 
793         updateBubbleZOrdersAndDotPosition(false /* animate */);
794     }
795 
796     /**
797      * Changes the currently selected bubble. If the stack is already expanded, the newly selected
798      * bubble will be shown immediately. This does not change the expanded state or change the
799      * position of any bubble.
800      */
801     // via BubbleData.Listener
setSelectedBubble(@ullable Bubble bubbleToSelect)802     public void setSelectedBubble(@Nullable Bubble bubbleToSelect) {
803         if (DEBUG_BUBBLE_STACK_VIEW) {
804             Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
805         }
806         if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
807             return;
808         }
809         final Bubble previouslySelected = mExpandedBubble;
810         mExpandedBubble = bubbleToSelect;
811 
812         if (mIsExpanded) {
813             // Make the container of the expanded view transparent before removing the expanded view
814             // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
815             // expanded view becomes visible on the screen. See b/126856255
816             mExpandedViewContainer.setAlpha(0.0f);
817             mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
818                 if (previouslySelected != null) {
819                     previouslySelected.setContentVisibility(false);
820                 }
821                 updateExpandedBubble();
822                 updatePointerPosition();
823                 requestUpdate();
824                 logBubbleEvent(previouslySelected, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
825                 logBubbleEvent(bubbleToSelect, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
826                 notifyExpansionChanged(previouslySelected, false /* expanded */);
827                 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
828             });
829         }
830     }
831 
832     /**
833      * Changes the expanded state of the stack.
834      *
835      * @param shouldExpand whether the bubble stack should appear expanded
836      */
837     // via BubbleData.Listener
setExpanded(boolean shouldExpand)838     public void setExpanded(boolean shouldExpand) {
839         if (DEBUG_BUBBLE_STACK_VIEW) {
840             Log.d(TAG, "setExpanded: " + shouldExpand);
841         }
842         if (shouldExpand == mIsExpanded) {
843             return;
844         }
845         if (mIsExpanded) {
846             animateCollapse();
847             logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
848         } else {
849             animateExpansion();
850             // TODO: move next line to BubbleData
851             logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
852             logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
853         }
854         notifyExpansionChanged(mExpandedBubble, mIsExpanded);
855     }
856 
857     /**
858      * Dismiss the stack of bubbles.
859      *
860      * @deprecated
861      */
862     @Deprecated
stackDismissed(int reason)863     void stackDismissed(int reason) {
864         if (DEBUG_BUBBLE_STACK_VIEW) {
865             Log.d(TAG, "stackDismissed: reason=" + reason);
866         }
867         mBubbleData.dismissAll(reason);
868         logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
869                 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
870     }
871 
872     /**
873      * @return the view the touch event is on
874      */
875     @Nullable
getTargetView(MotionEvent event)876     public View getTargetView(MotionEvent event) {
877         float x = event.getRawX();
878         float y = event.getRawY();
879         if (mIsExpanded) {
880             if (isIntersecting(mBubbleContainer, x, y)) {
881                 // Could be tapping or dragging a bubble while expanded
882                 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
883                     BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
884                     if (isIntersecting(view, x, y)) {
885                         return view;
886                     }
887                 }
888             }
889             BubbleExpandedView bev = (BubbleExpandedView) mExpandedViewContainer.getChildAt(0);
890             if (bev.intersectingTouchableContent((int) x, (int) y)) {
891                 return bev;
892             }
893             // Outside of the parts we care about.
894             return null;
895         } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
896             return mFlyout;
897         }
898         // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack.
899         return this;
900     }
901 
getFlyoutView()902     View getFlyoutView() {
903         return mFlyout;
904     }
905 
906     /**
907      * Collapses the stack of bubbles.
908      * <p>
909      * Must be called from the main thread.
910      *
911      * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
912      */
913     @Deprecated
914     @MainThread
collapseStack()915     void collapseStack() {
916         if (DEBUG_BUBBLE_STACK_VIEW) {
917             Log.d(TAG, "collapseStack()");
918         }
919         mBubbleData.setExpanded(false);
920     }
921 
922     /**
923      * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
924      */
925     @Deprecated
926     @MainThread
collapseStack(Runnable endRunnable)927     void collapseStack(Runnable endRunnable) {
928         if (DEBUG_BUBBLE_STACK_VIEW) {
929             Log.d(TAG, "collapseStack(endRunnable)");
930         }
931         collapseStack();
932         // TODO - use the runnable at end of animation
933         endRunnable.run();
934     }
935 
936     /**
937      * Expands the stack of bubbles.
938      * <p>
939      * Must be called from the main thread.
940      *
941      * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
942      */
943     @Deprecated
944     @MainThread
expandStack()945     void expandStack() {
946         if (DEBUG_BUBBLE_STACK_VIEW) {
947             Log.d(TAG, "expandStack()");
948         }
949         mBubbleData.setExpanded(true);
950     }
951 
beforeExpandedViewAnimation()952     private void beforeExpandedViewAnimation() {
953         hideFlyoutImmediate();
954         updateExpandedBubble();
955         updateExpandedView();
956         mIsExpansionAnimating = true;
957     }
958 
afterExpandedViewAnimation()959     private void afterExpandedViewAnimation() {
960         updateExpandedView();
961         mIsExpansionAnimating = false;
962         requestUpdate();
963     }
964 
animateCollapse()965     private void animateCollapse() {
966         mIsExpanded = false;
967         final Bubble previouslySelected = mExpandedBubble;
968         beforeExpandedViewAnimation();
969 
970         mBubbleContainer.cancelAllAnimations();
971         mExpandedAnimationController.collapseBackToStack(
972                 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
973                 /* collapseTo */,
974                 () -> {
975                     mBubbleContainer.setActiveController(mStackAnimationController);
976                     afterExpandedViewAnimation();
977                     previouslySelected.setContentVisibility(false);
978                 });
979 
980         mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
981         mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
982         mExpandedViewContainer.animate()
983                 .setDuration(100)
984                 .alpha(0f);
985     }
986 
animateExpansion()987     private void animateExpansion() {
988         mIsExpanded = true;
989         beforeExpandedViewAnimation();
990 
991         mBubbleContainer.setActiveController(mExpandedAnimationController);
992         mExpandedAnimationController.expandFromStack(() -> {
993             updatePointerPosition();
994             afterExpandedViewAnimation();
995         } /* after */);
996 
997 
998         mExpandedViewContainer.setTranslationX(getCollapsedX());
999         mExpandedViewContainer.setTranslationY(getCollapsedY());
1000         mExpandedViewContainer.setAlpha(0f);
1001 
1002         mExpandedViewXAnim.animateToFinalPosition(0f);
1003         mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1004         mExpandedViewContainer.animate()
1005                 .setDuration(100)
1006                 .alpha(1f);
1007     }
1008 
getCollapsedX()1009     private float getCollapsedX() {
1010         return mStackAnimationController.getStackPosition().x < getWidth() / 2
1011                 ? -mExpandedAnimateXDistance
1012                 : mExpandedAnimateXDistance;
1013     }
1014 
getCollapsedY()1015     private float getCollapsedY() {
1016         return Math.min(mStackAnimationController.getStackPosition().y,
1017                 mExpandedAnimateYDistance);
1018     }
1019 
notifyExpansionChanged(Bubble bubble, boolean expanded)1020     private void notifyExpansionChanged(Bubble bubble, boolean expanded) {
1021         if (mExpandListener != null && bubble != null) {
1022             mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
1023         }
1024     }
1025 
1026     /** Return the BubbleView at the given index from the bubble container. */
getBubbleAt(int i)1027     public BubbleView getBubbleAt(int i) {
1028         return mBubbleContainer.getChildCount() > i
1029                 ? (BubbleView) mBubbleContainer.getChildAt(i)
1030                 : null;
1031     }
1032 
1033     /** Moves the bubbles out of the way if they're going to be over the keyboard. */
onImeVisibilityChanged(boolean visible, int height)1034     public void onImeVisibilityChanged(boolean visible, int height) {
1035         mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
1036 
1037         if (!mIsExpanded) {
1038             mStackAnimationController.animateForImeVisibility(visible);
1039         }
1040     }
1041 
1042     /** Called when a drag operation on an individual bubble has started. */
onBubbleDragStart(View bubble)1043     public void onBubbleDragStart(View bubble) {
1044         if (DEBUG_BUBBLE_STACK_VIEW) {
1045             Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
1046         }
1047         mExpandedAnimationController.prepareForBubbleDrag(bubble);
1048     }
1049 
1050     /** Called with the coordinates to which an individual bubble has been dragged. */
onBubbleDragged(View bubble, float x, float y)1051     public void onBubbleDragged(View bubble, float x, float y) {
1052         if (!mIsExpanded || mIsExpansionAnimating) {
1053             return;
1054         }
1055 
1056         mExpandedAnimationController.dragBubbleOut(bubble, x, y);
1057         springInDismissTarget();
1058     }
1059 
1060     /** Called when a drag operation on an individual bubble has finished. */
onBubbleDragFinish( View bubble, float x, float y, float velX, float velY)1061     public void onBubbleDragFinish(
1062             View bubble, float x, float y, float velX, float velY) {
1063         if (DEBUG_BUBBLE_STACK_VIEW) {
1064             Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
1065         }
1066 
1067         if (!mIsExpanded || mIsExpansionAnimating) {
1068             return;
1069         }
1070 
1071         mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
1072         hideDismissTarget();
1073     }
1074 
onDragStart()1075     void onDragStart() {
1076         if (DEBUG_BUBBLE_STACK_VIEW) {
1077             Log.d(TAG, "onDragStart()");
1078         }
1079         if (mIsExpanded || mIsExpansionAnimating) {
1080             return;
1081         }
1082 
1083         mStackAnimationController.cancelStackPositionAnimations();
1084         mBubbleContainer.setActiveController(mStackAnimationController);
1085         hideFlyoutImmediate();
1086 
1087         mDraggingInDismissTarget = false;
1088     }
1089 
onDragged(float x, float y)1090     void onDragged(float x, float y) {
1091         if (mIsExpanded || mIsExpansionAnimating) {
1092             return;
1093         }
1094 
1095         springInDismissTarget();
1096         mStackAnimationController.moveStackFromTouch(x, y);
1097     }
1098 
onDragFinish(float x, float y, float velX, float velY)1099     void onDragFinish(float x, float y, float velX, float velY) {
1100         if (DEBUG_BUBBLE_STACK_VIEW) {
1101             Log.d(TAG, "onDragFinish");
1102         }
1103 
1104         if (mIsExpanded || mIsExpansionAnimating) {
1105             return;
1106         }
1107 
1108         final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
1109         logBubbleEvent(null /* no bubble associated with bubble stack move */,
1110                 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
1111 
1112         mStackOnLeftOrWillBe = newStackX <= 0;
1113         updateBubbleZOrdersAndDotPosition(true /* animate */);
1114         hideDismissTarget();
1115     }
1116 
onFlyoutDragStart()1117     void onFlyoutDragStart() {
1118         mFlyout.removeCallbacks(mHideFlyout);
1119     }
1120 
onFlyoutDragged(float deltaX)1121     void onFlyoutDragged(float deltaX) {
1122         // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1123         // is continually called.
1124         if (mFlyout.getWidth() <= 0) {
1125             return;
1126         }
1127 
1128         final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1129         mFlyoutDragDeltaX = deltaX;
1130 
1131         final float collapsePercent =
1132                 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1133         mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1134 
1135         // Calculate how to translate the flyout if it has been dragged too far in either direction.
1136         float overscrollTranslation = 0f;
1137         if (collapsePercent < 0f || collapsePercent > 1f) {
1138             // Whether we are more than 100% transitioned to the dot.
1139             final boolean overscrollingPastDot = collapsePercent > 1f;
1140 
1141             // Whether we are overscrolling physically to the left - this can either be pulling the
1142             // flyout away from the stack (if the stack is on the right) or pushing it to the left
1143             // after it has already become the dot.
1144             final boolean overscrollingLeft =
1145                     (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
1146             overscrollTranslation =
1147                     (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1148                             * (overscrollingLeft ? -1 : 1)
1149                             * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
1150                             // Attenuate the smaller dot less than the larger flyout.
1151                             / (overscrollingPastDot ? 2 : 1)));
1152         }
1153 
1154         mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1155     }
1156 
1157     /**
1158      * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
1159      * once it collapses.
1160      */
1161     @Nullable private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
1162 
onFlyoutTapped()1163     void onFlyoutTapped() {
1164         mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
1165 
1166         mFlyout.removeCallbacks(mHideFlyout);
1167         mHideFlyout.run();
1168     }
1169 
1170     /**
1171      * Called when the flyout drag has finished, and returns true if the gesture successfully
1172      * dismissed the flyout.
1173      */
onFlyoutDragFinished(float deltaX, float velX)1174     void onFlyoutDragFinished(float deltaX, float velX) {
1175         final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1176         final boolean metRequiredVelocity =
1177                 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
1178         final boolean metRequiredDeltaX =
1179                 onLeft
1180                         ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
1181                         : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
1182         final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
1183         final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling);
1184 
1185         mFlyout.removeCallbacks(mHideFlyout);
1186         animateFlyoutCollapsed(shouldDismiss, velX);
1187     }
1188 
1189     /**
1190      * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.)
1191      * is received.
1192      */
1193     void onGestureStart() {
1194         mIsGestureInProgress = true;
1195     }
1196 
1197     /** Called when a gesture is completed or cancelled. */
1198     void onGestureFinished() {
1199         mIsGestureInProgress = false;
1200 
1201         if (mIsExpanded) {
1202             mExpandedAnimationController.onGestureFinished();
1203         }
1204     }
1205 
1206     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1207     private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1208         mDesaturateAndDarkenTargetView = targetView;
1209 
1210         if (desaturateAndDarken) {
1211             // Use the animated paint for the bubbles.
1212             mDesaturateAndDarkenTargetView.setLayerType(
1213                     View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1214             mDesaturateAndDarkenAnimator.removeAllListeners();
1215             mDesaturateAndDarkenAnimator.start();
1216         } else {
1217             mDesaturateAndDarkenAnimator.removeAllListeners();
1218             mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1219                 @Override
1220                 public void onAnimationEnd(Animator animation) {
1221                     super.onAnimationEnd(animation);
1222                     // Stop using the animated paint.
1223                     resetDesaturationAndDarken();
1224                 }
1225             });
1226             mDesaturateAndDarkenAnimator.reverse();
1227         }
1228     }
1229 
1230     private void resetDesaturationAndDarken() {
1231         mDesaturateAndDarkenAnimator.removeAllListeners();
1232         mDesaturateAndDarkenAnimator.cancel();
1233         mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1234     }
1235 
1236     /**
1237      * Magnets the stack to the target, while also transforming the target to encircle the stack and
1238      * desaturating/darkening the bubbles.
1239      */
1240     void animateMagnetToDismissTarget(
1241             View magnetView, boolean toTarget, float x, float y, float velX, float velY) {
1242         mDraggingInDismissTarget = toTarget;
1243 
1244         if (toTarget) {
1245             // The Y-value for the bubble stack to be positioned in the center of the dismiss target
1246             final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f;
1247 
1248             mAnimatingMagnet = true;
1249 
1250             final Runnable afterMagnet = () -> {
1251                 mAnimatingMagnet = false;
1252                 if (mAfterMagnet != null) {
1253                     mAfterMagnet.run();
1254                 }
1255             };
1256 
1257             if (magnetView == this) {
1258                 mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet);
1259                 animateDesaturateAndDarken(mBubbleContainer, true);
1260             } else {
1261                 mExpandedAnimationController.magnetBubbleToDismiss(
1262                         magnetView, velX, velY, destY, afterMagnet);
1263 
1264                 animateDesaturateAndDarken(magnetView, true);
1265             }
1266         } else {
1267             mAnimatingMagnet = false;
1268 
1269             if (magnetView == this) {
mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY)1270                 mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY);
animateDesaturateAndDarken(mBubbleContainer, false)1271                 animateDesaturateAndDarken(mBubbleContainer, false);
1272             } else {
mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY)1273                 mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY);
animateDesaturateAndDarken(magnetView, false)1274                 animateDesaturateAndDarken(magnetView, false);
1275             }
1276         }
1277 
1278         mVibrator.vibrate(VibrationEffect.get(toTarget
1279                 ? VibrationEffect.EFFECT_CLICK
1280                 : VibrationEffect.EFFECT_TICK));
1281     }
1282 
1283     /**
1284      * Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack
1285      * using the 'implode' animation and animate out the target.
1286      */
magnetToStackIfNeededThenAnimateDismissal( View touchedView, float velX, float velY, Runnable after)1287     void magnetToStackIfNeededThenAnimateDismissal(
1288             View touchedView, float velX, float velY, Runnable after) {
1289         final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
1290         final Runnable animateDismissal = () -> {
1291             mAfterMagnet = null;
1292 
1293             mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
1294             mDismissContainer.springOut();
1295 
1296             // 'Implode' the stack and then hide the dismiss target.
1297             if (touchedView == this) {
1298                 mStackAnimationController.implodeStack(
1299                         () -> {
1300                             mAnimatingMagnet = false;
1301                             mShowingDismiss = false;
1302                             mDraggingInDismissTarget = false;
1303                             after.run();
1304                             resetDesaturationAndDarken();
1305                         });
1306             } else {
1307                 mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
1308                     mAnimatingMagnet = false;
1309                     mShowingDismiss = false;
1310                     mDraggingInDismissTarget = false;
1311                     resetDesaturationAndDarken();
1312                     after.run();
1313                 });
1314             }
1315         };
1316 
1317         if (mAnimatingMagnet) {
1318             // If the magnet animation is currently playing, dismiss the stack after it's done. This
1319             // happens if the stack is flung towards the target.
1320             mAfterMagnet = animateDismissal;
1321         } else if (mDraggingInDismissTarget) {
1322             // If we're in the dismiss target, but not animating, we already magneted - dismiss
1323             // immediately.
1324             animateDismissal.run();
1325         } else {
1326             // Otherwise, we need to start the magnet animation and then dismiss afterward.
1327             animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY);
1328             mAfterMagnet = animateDismissal;
1329         }
1330     }
1331 
1332     /** Animates in the dismiss target. */
springInDismissTarget()1333     private void springInDismissTarget() {
1334         if (mShowingDismiss) {
1335             return;
1336         }
1337 
1338         mShowingDismiss = true;
1339 
1340         // Show the dismiss container and bring it to the front so the bubbles will go behind it.
1341         mDismissContainer.springIn();
1342         mDismissContainer.bringToFront();
1343         mDismissContainer.setZ(Short.MAX_VALUE - 1);
1344     }
1345 
1346     /**
1347      * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1348      * were dragged into the target and encircled.
1349      */
hideDismissTarget()1350     private void hideDismissTarget() {
1351         if (!mShowingDismiss) {
1352             return;
1353         }
1354 
1355         mDismissContainer.springOut();
1356         mShowingDismiss = false;
1357     }
1358 
1359     /** Whether the location of the given MotionEvent is within the dismiss target area. */
isInDismissTarget(MotionEvent ev)1360     boolean isInDismissTarget(MotionEvent ev) {
1361         return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
1362     }
1363 
1364     /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
animateFlyoutCollapsed(boolean collapsed, float velX)1365     private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1366         final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1367         // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1368         // faster.
1369         mFlyoutTransitionSpring.getSpring().setStiffness(
1370                 (mBubbleToExpandAfterFlyoutCollapse != null)
1371                         ? SpringForce.STIFFNESS_MEDIUM
1372                         : SpringForce.STIFFNESS_LOW);
1373         mFlyoutTransitionSpring
1374                 .setStartValue(mFlyoutDragDeltaX)
1375                 .setStartVelocity(velX)
1376                 .animateToFinalPosition(collapsed
1377                         ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1378                         : 0f);
1379     }
1380 
1381     /** Updates the dot visibility, this is used in response to a zen mode config change. */
updateDots()1382     void updateDots() {
1383         int bubbsCount = mBubbleContainer.getChildCount();
1384         for (int i = 0; i < bubbsCount; i++) {
1385             BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
1386             // If nothing changed the animation won't happen
1387             bv.updateDotVisibility(true /* animate */);
1388         }
1389     }
1390 
1391     /**
1392      * Calculates the y position of the expanded view when it is expanded.
1393      */
getExpandedViewY()1394     float getExpandedViewY() {
1395         return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
1396     }
1397 
1398     /**
1399      * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1400      */
1401     @VisibleForTesting
animateInFlyoutForBubble(Bubble bubble)1402     void animateInFlyoutForBubble(Bubble bubble) {
1403         final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
1404         if (!bubble.showFlyoutForBubble()) {
1405             // In case flyout was suppressed for this update, reset now.
1406             bubble.setSuppressFlyout(false);
1407             return;
1408         }
1409         if (updateMessage == null
1410                 || isExpanded()
1411                 || mIsExpansionAnimating
1412                 || mIsGestureInProgress
1413                 || mBubbleToExpandAfterFlyoutCollapse != null
1414                 || bubble.getIconView() == null) {
1415             // Skip the message if none exists, we're expanded or animating expansion, or we're
1416             // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
1417             return;
1418         }
1419         mFlyoutDragDeltaX = 0f;
1420         clearFlyoutOnHide();
1421         mFlyoutOnHide = () -> {
1422             resetDot(bubble);
1423             if (mBubbleToExpandAfterFlyoutCollapse == null) {
1424                 return;
1425             }
1426             mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1427             mBubbleData.setExpanded(true);
1428             mBubbleToExpandAfterFlyoutCollapse = null;
1429         };
1430         mFlyout.setVisibility(INVISIBLE);
1431 
1432         // Temporarily suppress the dot while the flyout is visible.
1433         bubble.getIconView().setSuppressDot(
1434                 true /* suppressDot */, false /* animate */);
1435 
1436         // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1437         post(() -> {
1438             // An auto-expanding bubble could have been posted during the time it takes to
1439             // layout.
1440             if (isExpanded()) {
1441                 return;
1442             }
1443             final Runnable expandFlyoutAfterDelay = () -> {
1444                 mAnimateInFlyout = () -> {
1445                     mFlyout.setVisibility(VISIBLE);
1446                     mFlyoutDragDeltaX =
1447                             mStackAnimationController.isStackOnLeftSide()
1448                                     ? -mFlyout.getWidth()
1449                                     : mFlyout.getWidth();
1450                     animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1451                     mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
1452                 };
1453                 mFlyout.postDelayed(mAnimateInFlyout, 200);
1454             };
1455             mFlyout.setupFlyoutStartingAsDot(
1456                     updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
1457                     mStackAnimationController.isStackOnLeftSide(),
1458                     bubble.getIconView().getBadgeColor() /* dotColor */,
1459                     expandFlyoutAfterDelay /* onLayoutComplete */,
1460                     mFlyoutOnHide,
1461                     bubble.getIconView().getDotCenter());
1462             mFlyout.bringToFront();
1463         });
1464         mFlyout.removeCallbacks(mHideFlyout);
1465         mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
1466         logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
1467     }
1468 
resetDot(Bubble bubble)1469     private void resetDot(Bubble bubble) {
1470         final boolean suppressDot = !bubble.showBubbleDot();
1471         // If we're going to suppress the dot, make it visible first so it'll
1472         // visibly animate away.
1473 
1474         if (suppressDot) {
1475             bubble.getIconView().setSuppressDot(
1476                     false /* suppressDot */, false /* animate */);
1477         }
1478         // Reset dot suppression. If we're not suppressing due to DND, then
1479         // stop suppressing it with no animation (since the flyout has
1480         // transformed into the dot). If we are suppressing due to DND, animate
1481         // it away.
1482         bubble.getIconView().setSuppressDot(
1483                 suppressDot /* suppressDot */,
1484                 suppressDot /* animate */);
1485     }
1486 
1487     /** Hide the flyout immediately and cancel any pending hide runnables. */
hideFlyoutImmediate()1488     private void hideFlyoutImmediate() {
1489         clearFlyoutOnHide();
1490         mFlyout.removeCallbacks(mAnimateInFlyout);
1491         mFlyout.removeCallbacks(mHideFlyout);
1492         mFlyout.hideFlyout();
1493     }
1494 
clearFlyoutOnHide()1495     private void clearFlyoutOnHide() {
1496         mFlyout.removeCallbacks(mAnimateInFlyout);
1497         if (mFlyoutOnHide == null) {
1498             return;
1499         }
1500         mFlyoutOnHide.run();
1501         mFlyoutOnHide = null;
1502     }
1503 
1504     @Override
getBoundsOnScreen(Rect outRect)1505     public void getBoundsOnScreen(Rect outRect) {
1506         if (!mIsExpanded) {
1507             if (mBubbleContainer.getChildCount() > 0) {
1508                 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1509             }
1510             // Increase the touch target size of the bubble
1511             outRect.top -= mBubbleTouchPadding;
1512             outRect.left -= mBubbleTouchPadding;
1513             outRect.right += mBubbleTouchPadding;
1514             outRect.bottom += mBubbleTouchPadding;
1515         } else {
1516             mBubbleContainer.getBoundsOnScreen(outRect);
1517         }
1518 
1519         if (mFlyout.getVisibility() == View.VISIBLE) {
1520             final Rect flyoutBounds = new Rect();
1521             mFlyout.getBoundsOnScreen(flyoutBounds);
1522             outRect.union(flyoutBounds);
1523         }
1524     }
1525 
getStatusBarHeight()1526     private int getStatusBarHeight() {
1527         if (getRootWindowInsets() != null) {
1528             WindowInsets insets = getRootWindowInsets();
1529             return Math.max(
1530                     mStatusBarHeight,
1531                     insets.getDisplayCutout() != null
1532                             ? insets.getDisplayCutout().getSafeInsetTop()
1533                             : 0);
1534         }
1535 
1536         return 0;
1537     }
1538 
isIntersecting(View view, float x, float y)1539     private boolean isIntersecting(View view, float x, float y) {
1540         mTempLoc = view.getLocationOnScreen();
1541         mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
1542                 mTempLoc[1] + view.getHeight());
1543         return mTempRect.contains(x, y);
1544     }
1545 
requestUpdate()1546     private void requestUpdate() {
1547         if (mViewUpdatedRequested || mIsExpansionAnimating) {
1548             return;
1549         }
1550         mViewUpdatedRequested = true;
1551         getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1552         invalidate();
1553     }
1554 
updateExpandedBubble()1555     private void updateExpandedBubble() {
1556         if (DEBUG_BUBBLE_STACK_VIEW) {
1557             Log.d(TAG, "updateExpandedBubble()");
1558         }
1559         mExpandedViewContainer.removeAllViews();
1560         if (mExpandedBubble != null && mIsExpanded) {
1561             mExpandedViewContainer.addView(mExpandedBubble.getExpandedView());
1562             mExpandedBubble.getExpandedView().populateExpandedView();
1563             mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
1564             mExpandedViewContainer.setAlpha(1.0f);
1565         }
1566     }
1567 
updateExpandedView()1568     private void updateExpandedView() {
1569         if (DEBUG_BUBBLE_STACK_VIEW) {
1570             Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
1571         }
1572 
1573         mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
1574         if (mIsExpanded) {
1575             // First update the view so that it calculates a new height (ensuring the y position
1576             // calculation is correct)
1577             mExpandedBubble.getExpandedView().updateView();
1578             final float y = getExpandedViewY();
1579             if (!mExpandedViewYAnim.isRunning()) {
1580                 // We're not animating so set the value
1581                 mExpandedViewContainer.setTranslationY(y);
1582                 mExpandedBubble.getExpandedView().updateView();
1583             } else {
1584                 // We are animating so update the value; there is an end listener on the animator
1585                 // that will ensure expandedeView.updateView gets called.
1586                 mExpandedViewYAnim.animateToFinalPosition(y);
1587             }
1588         }
1589 
1590         mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
1591         updateBubbleZOrdersAndDotPosition(false);
1592     }
1593 
1594     /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
updateBubbleZOrdersAndDotPosition(boolean animate)1595     private void updateBubbleZOrdersAndDotPosition(boolean animate) {
1596         int bubbleCount = mBubbleContainer.getChildCount();
1597         for (int i = 0; i < bubbleCount; i++) {
1598             BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
1599             bv.updateDotVisibility(true /* animate */);
1600             bv.setZ((BubbleController.MAX_BUBBLES
1601                     * getResources().getDimensionPixelSize(R.dimen.bubble_elevation)) - i);
1602             // If the dot is on the left, and so is the stack, we need to change the dot position.
1603             if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
1604                 bv.setDotPosition(!mStackOnLeftOrWillBe, animate);
1605             }
1606         }
1607     }
1608 
updatePointerPosition()1609     private void updatePointerPosition() {
1610         if (DEBUG_BUBBLE_STACK_VIEW) {
1611             Log.d(TAG, "updatePointerPosition()");
1612         }
1613 
1614         Bubble expandedBubble = getExpandedBubble();
1615         if (expandedBubble == null) {
1616             return;
1617         }
1618 
1619         int index = getBubbleIndex(expandedBubble);
1620         float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1621         float halfBubble = mBubbleSize / 2f;
1622         float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
1623         // Padding might be adjusted for insets, so get it directly from the view
1624         bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
1625 
1626         expandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
1627     }
1628 
1629     /**
1630      * @return the number of bubbles in the stack view.
1631      */
getBubbleCount()1632     public int getBubbleCount() {
1633         return mBubbleContainer.getChildCount();
1634     }
1635 
1636     /**
1637      * Finds the bubble index within the stack.
1638      *
1639      * @param bubble the bubble to look up.
1640      * @return the index of the bubble view within the bubble stack. The range of the position
1641      * is between 0 and the bubble count minus 1.
1642      */
getBubbleIndex(@ullable Bubble bubble)1643     int getBubbleIndex(@Nullable Bubble bubble) {
1644         if (bubble == null) {
1645             return 0;
1646         }
1647         return mBubbleContainer.indexOfChild(bubble.getIconView());
1648     }
1649 
1650     /**
1651      * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
1652      */
getNormalizedXPosition()1653     public float getNormalizedXPosition() {
1654         return new BigDecimal(getStackPosition().x / mDisplaySize.x)
1655                 .setScale(4, RoundingMode.CEILING.HALF_UP)
1656                 .floatValue();
1657     }
1658 
1659     /**
1660      * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
1661      */
getNormalizedYPosition()1662     public float getNormalizedYPosition() {
1663         return new BigDecimal(getStackPosition().y / mDisplaySize.y)
1664                 .setScale(4, RoundingMode.CEILING.HALF_UP)
1665                 .floatValue();
1666     }
1667 
getStackPosition()1668     public PointF getStackPosition() {
1669         return mStackAnimationController.getStackPosition();
1670     }
1671 
1672     /**
1673      * Logs the bubble UI event.
1674      *
1675      * @param bubble the bubble that is being interacted on. Null value indicates that
1676      *               the user interaction is not specific to one bubble.
1677      * @param action the user interaction enum.
1678      */
logBubbleEvent(@ullable Bubble bubble, int action)1679     private void logBubbleEvent(@Nullable Bubble bubble, int action) {
1680         if (bubble == null || bubble.getEntry() == null
1681                 || bubble.getEntry().notification == null) {
1682             StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
1683                     null /* package name */,
1684                     null /* notification channel */,
1685                     0 /* notification ID */,
1686                     0 /* bubble position */,
1687                     getBubbleCount(),
1688                     action,
1689                     getNormalizedXPosition(),
1690                     getNormalizedYPosition(),
1691                     false /* unread bubble */,
1692                     false /* on-going bubble */,
1693                     false /* isAppForeground (unused) */);
1694         } else {
1695             StatusBarNotification notification = bubble.getEntry().notification;
1696             StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
1697                     notification.getPackageName(),
1698                     notification.getNotification().getChannelId(),
1699                     notification.getId(),
1700                     getBubbleIndex(bubble),
1701                     getBubbleCount(),
1702                     action,
1703                     getNormalizedXPosition(),
1704                     getNormalizedYPosition(),
1705                     bubble.showInShadeWhenBubble(),
1706                     bubble.isOngoing(),
1707                     false /* isAppForeground (unused) */);
1708         }
1709     }
1710 
1711     /**
1712      * Called when a back gesture should be directed to the Bubbles stack. When expanded,
1713      * a back key down/up event pair is forwarded to the bubble Activity.
1714      */
performBackPressIfNeeded()1715     boolean performBackPressIfNeeded() {
1716         if (!isExpanded()) {
1717             return false;
1718         }
1719         return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
1720     }
1721 
1722     /** For debugging only */
getBubblesOnScreen()1723     List<Bubble> getBubblesOnScreen() {
1724         List<Bubble> bubbles = new ArrayList<>();
1725         for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
1726             View child = mBubbleContainer.getChildAt(i);
1727             if (child instanceof BubbleView) {
1728                 String key = ((BubbleView) child).getKey();
1729                 Bubble bubble = mBubbleData.getBubbleWithKey(key);
1730                 bubbles.add(bubble);
1731             }
1732         }
1733         return bubbles;
1734     }
1735 }
1736