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