Home | History | Annotate | Download | only in bubbles
      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          */
    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 
    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      */
    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. */
    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
    475     public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
    476         getBoundsOnScreen(outRect);
    477     }
    478 
    479     @Override
    480     protected void onDetachedFromWindow() {
    481         super.onDetachedFromWindow();
    482         getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
    483     }
    484 
    485     @Override
    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
    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
    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      */
    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 
    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      */
    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      */
    631     public void setExpandListener(BubbleController.BubbleExpandListener listener) {
    632         mExpandListener = listener;
    633     }
    634 
    635     /**
    636      * Whether the stack of bubbles is expanded or not.
    637      */
    638     public boolean isExpanded() {
    639         return mIsExpanded;
    640     }
    641 
    642     /**
    643      * The {@link BubbleView} that is expanded, null if one does not exist.
    644      */
    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      */
    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
    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
    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
    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
    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
    720     void updateBubble(Bubble bubble) {
    721         animateInFlyoutForBubble(bubble);
    722         requestUpdate();
    723         logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
    724     }
    725 
    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
    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
    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
    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
    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 
    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
    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
    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
    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      */
    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. */
    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. */
    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. */
    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. */
    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 
   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 
   1018     void onDragged(float x, float y) {
   1019         if (mIsExpanded || mIsExpansionAnimating) {
   1020             return;
   1021         }
   1022 
   1023         springInDismissTarget();
   1024         mStackAnimationController.moveStackFromTouch(x, y);
   1025     }
   1026 
   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 
   1045     void onFlyoutDragStart() {
   1046         mFlyout.removeCallbacks(mHideFlyout);
   1047     }
   1048 
   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      */
   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      */
   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. */
   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      */
   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. */
   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. */
   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      */
   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      */
   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      */
   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. */
   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      */
   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
   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. */
   1410     private void hideFlyoutImmediate() {
   1411         if (mAfterFlyoutHides != null) {
   1412             mAfterFlyoutHides.run();
   1413         }
   1414 
   1415         mFlyout.removeCallbacks(mHideFlyout);
   1416         mFlyout.hideFlyout();
   1417     }
   1418 
   1419     @Override
   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 
   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 
   1449     private int getBottomInset() {
   1450         if (getRootWindowInsets() != null) {
   1451             WindowInsets insets = getRootWindowInsets();
   1452             return insets.getSystemWindowInsetBottom();
   1453         }
   1454         return 0;
   1455     }
   1456 
   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 
   1464     private void requestUpdate() {
   1465         if (mViewUpdatedRequested || mIsExpansionAnimating) {
   1466             return;
   1467         }
   1468         mViewUpdatedRequested = true;
   1469         getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
   1470         invalidate();
   1471     }
   1472 
   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 
   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. */
   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 
   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      */
   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      */
   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      */
   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      */
   1594     public float getNormalizedYPosition() {
   1595         return new BigDecimal(getStackPosition().y / mDisplaySize.y)
   1596                 .setScale(4, RoundingMode.CEILING.HALF_UP)
   1597                 .floatValue();
   1598     }
   1599 
   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      */
   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      */
   1647     boolean performBackPressIfNeeded() {
   1648         if (!isExpanded()) {
   1649             return false;
   1650         }
   1651         return mExpandedBubble.expandedView.performBackPressIfNeeded();
   1652     }
   1653 
   1654     /** For debugging only */
   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