blob: f87bcef5fde2eff61ed3f590e43bd51e6ddb96d1 [file] [log] [blame]
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001/*
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
17package com.android.systemui.bubbles;
18
Joshua Tsujib1a796b2019-01-16 15:43:12 -080019import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080020import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21
Joshua Tsuji4accf5982019-04-22 17:36:11 -040022import android.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.ValueAnimator;
Issei Suzukic0387542019-03-08 17:31:14 +010025import android.annotation.NonNull;
Lyn Han6c40fe72019-05-08 14:06:33 -070026import android.app.Notification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080027import android.content.Context;
28import android.content.res.Resources;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040029import android.graphics.ColorMatrix;
30import android.graphics.ColorMatrixColorFilter;
Joshua Tsuji580c0bf2019-01-28 13:28:21 -050031import android.graphics.Outline;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040032import android.graphics.Paint;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080033import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080034import android.graphics.PointF;
35import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036import android.graphics.RectF;
Mady Mellor217b2e92019-02-27 11:44:16 -080037import android.os.Bundle;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040038import android.os.VibrationEffect;
39import android.os.Vibrator;
Steven Wua254dab2019-01-29 11:30:39 -050040import android.service.notification.StatusBarNotification;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050041import android.util.Log;
Steven Wua254dab2019-01-29 11:30:39 -050042import android.util.StatsLog;
Issei Suzukic0387542019-03-08 17:31:14 +010043import android.view.Choreographer;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -040044import android.view.Gravity;
Mady Mellordea7ecf2018-12-10 15:47:40 -080045import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080046import android.view.MotionEvent;
47import android.view.View;
Joshua Tsuji580c0bf2019-01-28 13:28:21 -050048import android.view.ViewOutlineProvider;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080049import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050050import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080051import android.view.WindowManager;
Mady Mellor217b2e92019-02-27 11:44:16 -080052import android.view.accessibility.AccessibilityNodeInfo;
Lyn Hane68d0912019-05-02 18:28:01 -070053import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040054import android.view.animation.AccelerateDecelerateInterpolator;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080055import android.widget.FrameLayout;
56
Mark Renoufcecc77b2019-01-30 16:32:24 -050057import androidx.annotation.MainThread;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080058import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080059import androidx.dynamicanimation.animation.DynamicAnimation;
Joshua Tsuji6549e702019-05-02 13:13:16 -040060import androidx.dynamicanimation.animation.FloatPropertyCompat;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080061import androidx.dynamicanimation.animation.SpringAnimation;
62import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080063
Mady Melloredd4ee12019-01-18 10:45:11 -080064import com.android.internal.annotations.VisibleForTesting;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080065import com.android.internal.widget.ViewClippingUtil;
66import com.android.systemui.R;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080067import com.android.systemui.bubbles.animation.ExpandedAnimationController;
68import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
69import com.android.systemui.bubbles.animation.StackAnimationController;
Ned Burnsf81c4c42019-01-07 14:10:43 -050070import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080071
Steven Wua254dab2019-01-29 11:30:39 -050072import java.math.BigDecimal;
73import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040074import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -040075import java.util.Collections;
76import java.util.List;
Steven Wua254dab2019-01-29 11:30:39 -050077
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080078/**
79 * Renders bubbles in a stack and handles animating expanded and collapsed states.
80 */
Joshua Tsuji442b6272019-02-08 13:23:43 -050081public class BubbleStackView extends FrameLayout {
Mark Renouf89b1a4a2018-12-04 14:59:45 -050082 private static final String TAG = "BubbleStackView";
Mark Renouf08bc42a2019-03-07 13:01:59 -050083 private static final boolean DEBUG = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080084
Joshua Tsuji6549e702019-05-02 13:13:16 -040085 /** 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
Joshua Tsuji614b1df2019-03-26 13:57:05 -040097 /** Duration of the flyout alpha animations. */
98 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
99
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400100 /** Percent to darken the bubbles when they're in the dismiss target. */
101 private static final float DARKEN_PERCENT = 0.3f;
102
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400103 /** How long to wait, in milliseconds, before hiding the flyout. */
104 @VisibleForTesting
105 static final int FLYOUT_HIDE_AFTER = 5000;
106
Issei Suzukic0387542019-03-08 17:31:14 +0100107 /**
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
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800144 private Point mDisplaySize;
145
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800146 private final SpringAnimation mExpandedViewXAnim;
147 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800148 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800149
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400150 private final Vibrator mVibrator;
151 private final ValueAnimator mDesaturateAndDarkenAnimator;
152 private final Paint mDesaturateAndDarkenPaint = new Paint();
153
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800154 private PhysicsAnimationLayout mBubbleContainer;
155 private StackAnimationController mStackAnimationController;
156 private ExpandedAnimationController mExpandedAnimationController;
157
Mady Mellor3dff9e62019-02-05 18:12:53 -0800158 private FrameLayout mExpandedViewContainer;
159
Joshua Tsuji6549e702019-05-02 13:13:16 -0400160 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400161 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400162 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400163
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400164 /** 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
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800174 private int mBubbleSize;
175 private int mBubblePadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700176 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800177 private int mExpandedAnimateXDistance;
178 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700179 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500180 private int mStatusBarHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800181 private int mPipDismissHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500182 private int mImeOffset;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800183
Mady Mellor3dff9e62019-02-05 18:12:53 -0800184 private Bubble mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800185 private boolean mIsExpanded;
Mady Mellor5d8f1402019-02-21 18:23:52 -0800186 private boolean mImeVisible;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800187
Joshua Tsuji6549e702019-05-02 13:13:16 -0400188 /** 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;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400193
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800194 private BubbleTouchHandler mTouchHandler;
Mady Mellorcd9b1302018-11-06 18:08:04 -0800195 private BubbleController.BubbleExpandListener mExpandListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800196 private BubbleExpandedView.OnBubbleBlockedListener mBlockedListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800197
198 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700199 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400200 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;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800214
Mady Mellor3dff9e62019-02-05 18:12:53 -0800215 private LayoutInflater mInflater;
216
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800217 // Used for determining view / touch intersection
218 int[] mTempLoc = new int[2];
219 RectF mTempRect = new RectF();
220
Mark Renouf821e6782019-04-01 14:17:37 -0400221 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
222
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800223 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
Mark Renouf821e6782019-04-01 14:17:37 -0400234 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
235 this::updateSystemGestureExcludeRects;
236
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800237 private ViewClippingUtil.ClippingParameters mClippingParameters =
238 new ViewClippingUtil.ClippingParameters() {
239
Lyn Han522e9ff2019-05-17 13:26:13 -0700240 @Override
241 public boolean shouldFinish(View view) {
242 return false;
243 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800244
Lyn Han522e9ff2019-05-17 13:26:13 -0700245 @Override
246 public boolean isClippingEnablingAllowed(View view) {
247 return !mIsExpanded;
248 }
249 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800250
Joshua Tsuji6549e702019-05-02 13:13:16 -0400251 /** 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
Issei Suzukic0387542019-03-08 17:31:14 +0100285 @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer;
286
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400287 private BubbleDismissView mDismissContainer;
288 private Runnable mAfterMagnet;
Issei Suzukic0387542019-03-08 17:31:14 +0100289
Joshua Tsujidd4d9f92019-05-13 13:57:38 -0400290 private boolean mSuppressNewDot = false;
291 private boolean mSuppressFlyout = false;
292
Issei Suzukic0387542019-03-08 17:31:14 +0100293 public BubbleStackView(Context context, BubbleData data,
Lyn Han522e9ff2019-05-17 13:26:13 -0700294 @Nullable SurfaceSynchronizer synchronizer) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800295 super(context);
296
Mady Mellorcfd06c12019-02-13 14:32:12 -0800297 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800298 mInflater = LayoutInflater.from(context);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400299 mTouchHandler = new BubbleTouchHandler(this, data, context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800300 setOnTouchListener(mTouchHandler);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500301 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800302
303 Resources res = getResources();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800304 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800305 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800306 mExpandedAnimateXDistance =
307 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
308 mExpandedAnimateYDistance =
309 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700310 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
311
Joshua Tsujif44347f2019-02-12 14:28:06 -0500312 mStatusBarHeight =
313 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800314 mPipDismissHeight = mContext.getResources().getDimensionPixelSize(
315 R.dimen.pip_dismiss_gradient_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500316 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800317
318 mDisplaySize = new Point();
319 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
320 wm.getDefaultDisplay().getSize(mDisplaySize);
321
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400322 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
323
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700324 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800325 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800326
327 mStackAnimationController = new StackAnimationController();
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700328 mExpandedAnimationController = new ExpandedAnimationController(
329 mDisplaySize, mExpandedViewPadding);
Issei Suzukic0387542019-03-08 17:31:14 +0100330 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800331
332 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400333 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800334 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800335 mBubbleContainer.setClipChildren(false);
336 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
337
Mady Mellor3dff9e62019-02-05 18:12:53 -0800338 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800339 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700340 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
341 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800342 mExpandedViewContainer.setClipChildren(false);
343 addView(mExpandedViewContainer);
344
Joshua Tsuji6549e702019-05-02 13:13:16 -0400345 mFlyout = new BubbleFlyoutView(context);
346 mFlyout.setVisibility(GONE);
347 mFlyout.animate()
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400348 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
349 .setInterpolator(new AccelerateDecelerateInterpolator());
Joshua Tsuji6549e702019-05-02 13:13:16 -0400350 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400351
Joshua Tsuji6549e702019-05-02 13:13:16 -0400352 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);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400363
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400364 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
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800371 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));
Mady Mellorbc078c22019-03-26 17:10:34 -0700384 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
385 if (mIsExpanded && mExpandedBubble != null) {
386 mExpandedBubble.expandedView.updateView();
387 }
388 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800389
390 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800391 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500392 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800393
394 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
395 final int keyboardHeight = insets.getSystemWindowInsetBottom()
396 - insets.getStableInsetBottom();
Mady Mellorbc078c22019-03-26 17:10:34 -0700397 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800398 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 });
Mark Renouf821e6782019-04-01 14:17:37 -0400415
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400416 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
Mark Renouf821e6782019-04-01 14:17:37 -0400425 // This must be a separate OnDrawListener since it should be called for every draw.
426 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400427
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 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800450 }
451
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700452 /**
Lyn Han02cca812019-04-02 16:27:32 -0700453 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700454 */
Lyn Han02cca812019-04-02 16:27:32 -0700455 public void onThemeChanged() {
Lyn Han76e803d2019-03-26 17:31:33 -0700456 for (Bubble b: mBubbleData.getBubbles()) {
Lyn Han80b80112019-04-04 14:03:40 -0700457 b.iconView.updateViews();
Mark Renouf34d04f32019-05-13 15:53:18 -0400458 b.expandedView.applyThemeAttrs();
Mady Mellore37f60b2019-03-25 14:48:34 -0700459 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700460 }
461
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400462 /** 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
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800474 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800475 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
476 getBoundsOnScreen(outRect);
477 }
478
479 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800480 protected void onDetachedFromWindow() {
481 super.onDetachedFromWindow();
482 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
483 }
484
485 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800486 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
Mady Mellor217b2e92019-02-27 11:44:16 -0800497 @Override
498 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
499 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Hane68d0912019-05-02 18:28:01 -0700500
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);
Mady Mellor217b2e92019-02-27 11:44:16 -0800524 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -0700525 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -0800526 } else {
Lyn Hane68d0912019-05-02 18:28:01 -0700527 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -0800528 }
529 }
530
531 @Override
532 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
533 if (super.performAccessibilityActionInternal(action, arguments)) {
534 return true;
535 }
Lyn Hane68d0912019-05-02 18:28:01 -0700536 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;
Mady Mellor217b2e92019-02-27 11:44:16 -0800560 }
561 return false;
562 }
563
Lyn Han6c40fe72019-05-08 14:06:33 -0700564 /**
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
Mark Renouf821e6782019-04-01 14:17:37 -0400601 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
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800617 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800618 * 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) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400622 Bubble b = mBubbleData.getBubbleWithKey(key);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800623 if (b != null) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400624 b.updateDotVisibility();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800625 }
626 }
627
628 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800629 * 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 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800636 * 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 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800645 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() {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800653 return mExpandedBubble;
654 }
655
656 /**
657 * Sets the bubble that should be expanded and expands if needed.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800658 *
659 * @param key the {@link NotificationEntry#key} associated with the bubble to expand.
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400660 * @deprecated replaced by setSelectedBubble(Bubble) + setExpanded(true)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800661 */
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400662 @Deprecated
Mady Mellor3dff9e62019-02-05 18:12:53 -0800663 void setExpandedBubble(String key) {
Mark Renouf71a3af62019-04-08 15:02:54 -0400664 Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key);
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400665 if (bubbleToExpand != null) {
666 setSelectedBubble(bubbleToExpand);
667 bubbleToExpand.entry.setShowInShadeWhenBubble(false);
668 setExpanded(true);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800669 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800670 }
671
672 /**
Mady Melloredd4ee12019-01-18 10:45:11 -0800673 * Sets the entry that should be expanded and expands if needed.
674 */
675 @VisibleForTesting
Mady Mellorc3d7d062019-03-28 16:13:05 -0700676 void setExpandedBubble(NotificationEntry entry) {
Mady Melloredd4ee12019-01-18 10:45:11 -0800677 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
678 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
679 if (entry.equals(bv.getEntry())) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800680 setExpandedBubble(entry.key);
Mady Melloredd4ee12019-01-18 10:45:11 -0800681 }
682 }
683 }
684
Mark Renouf71a3af62019-04-08 15:02:54 -0400685 // via BubbleData.Listener
686 void addBubble(Bubble bubble) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400687 if (DEBUG) {
688 Log.d(TAG, "addBubble: " + bubble);
689 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400690 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);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -0400694 if (bubble.iconView != null) {
695 bubble.iconView.setSuppressDot(mSuppressNewDot, false /* animate */);
696 }
Mark Renoufba5ab512019-05-02 15:21:01 -0400697 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400698 requestUpdate();
699 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Lyn Han522e9ff2019-05-17 13:26:13 -0700700 updatePointerPosition();
Mark Renouf71a3af62019-04-08 15:02:54 -0400701 }
702
703 // via BubbleData.Listener
704 void removeBubble(Bubble bubble) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400705 if (DEBUG) {
706 Log.d(TAG, "removeBubble: " + bubble);
707 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400708 // Remove it from the views
709 int removedIndex = mBubbleContainer.indexOfChild(bubble.iconView);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400710 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 }
Lyn Han522e9ff2019-05-17 13:26:13 -0700716 updatePointerPosition();
Mark Renouf71a3af62019-04-08 15:02:54 -0400717 }
718
719 // via BubbleData.Listener
720 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400721 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400722 requestUpdate();
723 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
724 }
725
Mark Renoufba5ab512019-05-02 15:21:01 -0400726 public void updateBubbleOrder(List<Bubble> bubbles) {
727 for (int i = 0; i < bubbles.size(); i++) {
728 Bubble bubble = bubbles.get(i);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400729 mBubbleContainer.reorderView(bubble.iconView, i);
Mark Renoufba5ab512019-05-02 15:21:01 -0400730 }
731 }
732
Mady Melloredd4ee12019-01-18 10:45:11 -0800733 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400734 * 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 */
Mark Renouf71a3af62019-04-08 15:02:54 -0400738 // via BubbleData.Listener
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400739 public void setSelectedBubble(@Nullable Bubble bubbleToSelect) {
740 if (DEBUG) {
741 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
742 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400743 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
744 return;
745 }
746 final Bubble previouslySelected = mExpandedBubble;
747 mExpandedBubble = bubbleToSelect;
Issei Suzukicac2a502019-04-16 16:52:50 +0200748
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400749 if (mIsExpanded) {
750 // Make the container of the expanded view transparent before removing the expanded view
751 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
752 // expanded view becomes visible on the screen. See b/126856255
753 mExpandedViewContainer.setAlpha(0.0f);
754 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Issei Suzukicac2a502019-04-16 16:52:50 +0200755 if (previouslySelected != null) {
756 previouslySelected.setContentVisibility(false);
757 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400758 updateExpandedBubble();
759 updatePointerPosition();
760 requestUpdate();
761 logBubbleEvent(previouslySelected, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
762 logBubbleEvent(bubbleToSelect, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
763 notifyExpansionChanged(previouslySelected.entry, false /* expanded */);
Mark Renouf71a3af62019-04-08 15:02:54 -0400764 notifyExpansionChanged(bubbleToSelect == null ? null : bubbleToSelect.entry,
765 true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400766 });
767 }
768 }
769
770 /**
771 * Changes the expanded state of the stack.
772 *
Mark Renouf71a3af62019-04-08 15:02:54 -0400773 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400774 */
Mark Renouf71a3af62019-04-08 15:02:54 -0400775 // via BubbleData.Listener
776 public void setExpanded(boolean shouldExpand) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400777 if (DEBUG) {
778 Log.d(TAG, "setExpanded: " + shouldExpand);
779 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400780 boolean wasExpanded = mIsExpanded;
781 if (shouldExpand == wasExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400782 return;
783 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400784 if (wasExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400785 // Collapse the stack
Issei Suzukicac2a502019-04-16 16:52:50 +0200786 mExpandedViewContainer.setAlpha(0.0f);
787 // TODO: In order to prevent flicker, code below should be executed after the alpha
788 // value set on the mExpandedViewContainer is reflected on the screen. However, we
789 // cannot just postpone the execution like #setSelectedBubble(), since some of member
790 // variables referred by the code are overridden before the execution.
791 if (mExpandedBubble != null) {
792 mExpandedBubble.setContentVisibility(false);
793 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400794 animateExpansion(false /* expand */);
795 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
796 } else {
797 // Expand the stack
798 animateExpansion(true /* expand */);
799 // TODO: move next line to BubbleData
800 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
801 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
802 }
803 notifyExpansionChanged(mExpandedBubble.entry, mIsExpanded);
804 }
805
806 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800807 * Dismiss the stack of bubbles.
Mark Renouf71a3af62019-04-08 15:02:54 -0400808 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800809 */
Mark Renouf71a3af62019-04-08 15:02:54 -0400810 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -0700811 void stackDismissed(int reason) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400812 if (DEBUG) {
813 Log.d(TAG, "stackDismissed: reason=" + reason);
814 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400815 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -0500816 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
817 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800818 }
819
820 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800821 * @return the view the touch event is on
822 */
823 @Nullable
824 public View getTargetView(MotionEvent event) {
825 float x = event.getRawX();
826 float y = event.getRawY();
827 if (mIsExpanded) {
828 if (isIntersecting(mBubbleContainer, x, y)) {
829 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
830 BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
831 if (isIntersecting(view, x, y)) {
832 return view;
833 }
834 }
835 } else if (isIntersecting(mExpandedViewContainer, x, y)) {
836 return mExpandedViewContainer;
837 }
838 // Outside parts of view we care about.
839 return null;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400840 } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400841 return mFlyout;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800842 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400843
844 // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800845 return this;
846 }
847
Mark Renouf71a3af62019-04-08 15:02:54 -0400848 View getFlyoutView() {
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400849 return mFlyout;
850 }
851
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800852 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800853 * Collapses the stack of bubbles.
Mark Renoufcecc77b2019-01-30 16:32:24 -0500854 * <p>
855 * Must be called from the main thread.
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400856 *
857 * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800858 */
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400859 @Deprecated
Mark Renoufcecc77b2019-01-30 16:32:24 -0500860 @MainThread
Mark Renouf71a3af62019-04-08 15:02:54 -0400861 void collapseStack() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400862 if (DEBUG) {
863 Log.d(TAG, "collapseStack()");
864 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400865 mBubbleData.setExpanded(false);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800866 }
867
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400868 /**
869 * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
870 */
871 @Deprecated
872 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -0800873 void collapseStack(Runnable endRunnable) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400874 if (DEBUG) {
875 Log.d(TAG, "collapseStack(endRunnable)");
876 }
Mady Mellor9801e852019-01-22 14:50:28 -0800877 collapseStack();
878 // TODO - use the runnable at end of animation
879 endRunnable.run();
880 }
881
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800882 /**
Mady Mellor94d94a72019-03-05 18:16:59 -0800883 * Expands the stack of bubbles.
Mark Renoufcecc77b2019-01-30 16:32:24 -0500884 * <p>
885 * Must be called from the main thread.
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400886 *
887 * @deprecated use {@link #setExpanded(boolean)} and {@link #setSelectedBubble(Bubble)}
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800888 */
Mark Renoufc6ab73d2019-04-09 16:42:22 -0400889 @Deprecated
Mark Renoufcecc77b2019-01-30 16:32:24 -0500890 @MainThread
Mark Renouf71a3af62019-04-08 15:02:54 -0400891 void expandStack() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400892 if (DEBUG) {
893 Log.d(TAG, "expandStack()");
894 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400895 mBubbleData.setExpanded(true);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800896 }
897
898 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800899 * Tell the stack to animate to collapsed or expanded state.
900 */
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800901 private void animateExpansion(boolean shouldExpand) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400902 if (DEBUG) {
903 Log.d(TAG, "animateExpansion: shouldExpand=" + shouldExpand);
904 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800905 if (mIsExpanded != shouldExpand) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400906 hideFlyoutImmediate();
907
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800908 mIsExpanded = shouldExpand;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800909 updateExpandedBubble();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800910 applyCurrentState();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800911
Mady Mellorbc078c22019-03-26 17:10:34 -0700912 mIsExpansionAnimating = true;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800913
914 Runnable updateAfter = () -> {
915 applyCurrentState();
Mady Mellorbc078c22019-03-26 17:10:34 -0700916 mIsExpansionAnimating = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800917 requestUpdate();
918 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800919
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800920 if (shouldExpand) {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400921 mBubbleContainer.setActiveController(mExpandedAnimationController);
Joshua Tsujif49ee142019-05-29 16:32:01 -0400922 mExpandedAnimationController.expandFromStack(() -> {
923 updatePointerPosition();
924 updateAfter.run();
925 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800926 } else {
927 mBubbleContainer.cancelAllAnimations();
928 mExpandedAnimationController.collapseBackToStack(
Joshua Tsujif49ee142019-05-29 16:32:01 -0400929 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800930 () -> {
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400931 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800932 updateAfter.run();
933 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800934 }
935
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800936 final float xStart =
937 mStackAnimationController.getStackPosition().x < getWidth() / 2
938 ? -mExpandedAnimateXDistance
939 : mExpandedAnimateXDistance;
940
941 final float yStart = Math.min(
942 mStackAnimationController.getStackPosition().y,
943 mExpandedAnimateYDistance);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800944 final float yDest = getYPositionForExpandedView();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800945
946 if (shouldExpand) {
947 mExpandedViewContainer.setTranslationX(xStart);
948 mExpandedViewContainer.setTranslationY(yStart);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800949 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800950
951 mExpandedViewXAnim.animateToFinalPosition(shouldExpand ? 0f : xStart);
952 mExpandedViewYAnim.animateToFinalPosition(shouldExpand ? yDest : yStart);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800953 }
954 }
955
Mady Mellor3dff9e62019-02-05 18:12:53 -0800956 private void notifyExpansionChanged(NotificationEntry entry, boolean expanded) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800957 if (mExpandListener != null) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800958 mExpandListener.onBubbleExpandChanged(expanded, entry != null ? entry.key : null);
959 }
960 }
961
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800962 /** Return the BubbleView at the given index from the bubble container. */
963 public BubbleView getBubbleAt(int i) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800964 return mBubbleContainer.getChildCount() > i
965 ? (BubbleView) mBubbleContainer.getChildAt(i)
966 : null;
967 }
968
Joshua Tsujia19515f2019-02-13 18:02:29 -0500969 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
970 public void onImeVisibilityChanged(boolean visible, int height) {
Joshua Tsuji4b395912019-04-19 17:18:40 -0400971 mStackAnimationController.setImeHeight(height + mImeOffset);
972
Joshua Tsujia19515f2019-02-13 18:02:29 -0500973 if (!mIsExpanded) {
Joshua Tsuji4b395912019-04-19 17:18:40 -0400974 mStackAnimationController.animateForImeVisibility(visible);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500975 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800976 }
977
978 /** Called when a drag operation on an individual bubble has started. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500979 public void onBubbleDragStart(View bubble) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400980 if (DEBUG) {
981 Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
982 }
Joshua Tsuji442b6272019-02-08 13:23:43 -0500983 mExpandedAnimationController.prepareForBubbleDrag(bubble);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800984 }
985
986 /** Called with the coordinates to which an individual bubble has been dragged. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500987 public void onBubbleDragged(View bubble, float x, float y) {
Mady Mellorbc078c22019-03-26 17:10:34 -0700988 if (!mIsExpanded || mIsExpansionAnimating) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500989 return;
990 }
991
992 mExpandedAnimationController.dragBubbleOut(bubble, x, y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400993 springInDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800994 }
995
996 /** Called when a drag operation on an individual bubble has finished. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500997 public void onBubbleDragFinish(
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400998 View bubble, float x, float y, float velX, float velY) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400999 if (DEBUG) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001000 Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001001 }
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001002
Mady Mellorbc078c22019-03-26 17:10:34 -07001003 if (!mIsExpanded || mIsExpansionAnimating) {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001004 return;
1005 }
1006
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001007 mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
1008 springOutDismissTargetAndHideCircle();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001009 }
1010
1011 void onDragStart() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001012 if (DEBUG) {
1013 Log.d(TAG, "onDragStart()");
1014 }
Mady Mellorbc078c22019-03-26 17:10:34 -07001015 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001016 return;
1017 }
1018
1019 mStackAnimationController.cancelStackPositionAnimations();
Joshua Tsujic36ee6f2019-05-28 17:00:16 -04001020 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001021 hideFlyoutImmediate();
1022
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001023 mDraggingInDismissTarget = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001024 }
1025
1026 void onDragged(float x, float y) {
Mady Mellorbc078c22019-03-26 17:10:34 -07001027 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001028 return;
1029 }
1030
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001031 springInDismissTarget();
1032 mStackAnimationController.moveStackFromTouch(x, y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001033 }
1034
1035 void onDragFinish(float x, float y, float velX, float velY) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001036 if (DEBUG) {
1037 Log.d(TAG, "onDragFinish");
1038 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001039
Mady Mellorbc078c22019-03-26 17:10:34 -07001040 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001041 return;
1042 }
1043
Joshua Tsuji6549e702019-05-02 13:13:16 -04001044 final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
Steven Wua254dab2019-01-29 11:30:39 -05001045 logBubbleEvent(null /* no bubble associated with bubble stack move */,
1046 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001047
Joshua Tsuji6549e702019-05-02 13:13:16 -04001048 mStackOnLeftOrWillBe = newStackX <= 0;
1049 updateBubbleShadowsAndDotPosition(true /* animate */);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001050 springOutDismissTargetAndHideCircle();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001051 }
1052
Joshua Tsuji6549e702019-05-02 13:13:16 -04001053 void onFlyoutDragStart() {
1054 mFlyout.removeCallbacks(mHideFlyout);
1055 }
1056
1057 void onFlyoutDragged(float deltaX) {
1058 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1059 mFlyoutDragDeltaX = deltaX;
1060
1061 final float collapsePercent =
1062 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1063 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1064
1065 // Calculate how to translate the flyout if it has been dragged too far in etiher direction.
1066 float overscrollTranslation = 0f;
1067 if (collapsePercent < 0f || collapsePercent > 1f) {
1068 // Whether we are more than 100% transitioned to the dot.
1069 final boolean overscrollingPastDot = collapsePercent > 1f;
1070
1071 // Whether we are overscrolling physically to the left - this can either be pulling the
1072 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1073 // after it has already become the dot.
1074 final boolean overscrollingLeft =
1075 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
1076
1077 overscrollTranslation =
1078 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1079 * (overscrollingLeft ? -1 : 1)
1080 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001081 // Attenuate the smaller dot less than the larger flyout.
1082 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001083 }
1084
1085 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1086 }
1087
1088 /**
1089 * Called when the flyout drag has finished, and returns true if the gesture successfully
1090 * dismissed the flyout.
1091 */
1092 void onFlyoutDragFinished(float deltaX, float velX) {
1093 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1094 final boolean metRequiredVelocity =
1095 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
1096 final boolean metRequiredDeltaX =
1097 onLeft
1098 ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
1099 : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
1100 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
1101 final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling);
1102
1103 mFlyout.removeCallbacks(mHideFlyout);
1104 animateFlyoutCollapsed(shouldDismiss, velX);
1105 }
1106
1107 /**
1108 * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.)
1109 * is received.
1110 */
1111 void onGestureStart() {
1112 mIsGestureInProgress = true;
1113 }
1114
1115 /** Called when a gesture is completed or cancelled. */
1116 void onGestureFinished() {
1117 mIsGestureInProgress = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -04001118
1119 if (mIsExpanded) {
1120 mExpandedAnimationController.onGestureFinished();
1121 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001122 }
1123
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001124 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1125 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1126 mDesaturateAndDarkenTargetView = targetView;
1127
1128 if (desaturateAndDarken) {
1129 // Use the animated paint for the bubbles.
1130 mDesaturateAndDarkenTargetView.setLayerType(
1131 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1132 mDesaturateAndDarkenAnimator.removeAllListeners();
1133 mDesaturateAndDarkenAnimator.start();
1134 } else {
1135 mDesaturateAndDarkenAnimator.removeAllListeners();
1136 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1137 @Override
1138 public void onAnimationEnd(Animator animation) {
1139 super.onAnimationEnd(animation);
1140 // Stop using the animated paint.
1141 resetDesaturationAndDarken();
1142 }
1143 });
1144 mDesaturateAndDarkenAnimator.reverse();
1145 }
1146 }
1147
1148 private void resetDesaturationAndDarken() {
1149 mDesaturateAndDarkenAnimator.removeAllListeners();
1150 mDesaturateAndDarkenAnimator.cancel();
1151 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1152 }
1153
1154 /**
1155 * Magnets the stack to the target, while also transforming the target to encircle the stack and
1156 * desaturating/darkening the bubbles.
1157 */
1158 void animateMagnetToDismissTarget(
1159 View magnetView, boolean toTarget, float x, float y, float velX, float velY) {
1160 mDraggingInDismissTarget = toTarget;
1161
1162 if (toTarget) {
1163 // The Y-value for the bubble stack to be positioned in the center of the dismiss target
1164 final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f;
1165
1166 mAnimatingMagnet = true;
1167
1168 final Runnable afterMagnet = () -> {
1169 mAnimatingMagnet = false;
1170 if (mAfterMagnet != null) {
1171 mAfterMagnet.run();
1172 }
1173 };
1174
1175 if (magnetView == this) {
1176 mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet);
1177 animateDesaturateAndDarken(mBubbleContainer, true);
1178 } else {
1179 mExpandedAnimationController.magnetBubbleToDismiss(
1180 magnetView, velX, velY, destY, afterMagnet);
1181
1182 animateDesaturateAndDarken(magnetView, true);
1183 }
1184
1185 mDismissContainer.animateEncircleCenterWithX(true);
1186
1187 } else {
1188 mAnimatingMagnet = false;
1189
1190 if (magnetView == this) {
1191 mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY);
1192 animateDesaturateAndDarken(mBubbleContainer, false);
1193 } else {
1194 mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY);
1195 animateDesaturateAndDarken(magnetView, false);
1196 }
1197
1198 mDismissContainer.animateEncircleCenterWithX(false);
1199 }
1200
1201 mVibrator.vibrate(VibrationEffect.get(toTarget
1202 ? VibrationEffect.EFFECT_CLICK
1203 : VibrationEffect.EFFECT_TICK));
1204 }
1205
1206 /**
1207 * Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack
1208 * using the 'implode' animation and animate out the target.
1209 */
1210 void magnetToStackIfNeededThenAnimateDismissal(
1211 View touchedView, float velX, float velY, Runnable after) {
Joshua Tsujif49ee142019-05-29 16:32:01 -04001212 final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001213 final Runnable animateDismissal = () -> {
1214 mAfterMagnet = null;
1215
1216 mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
1217 mDismissContainer.animateEncirclingCircleDisappearance();
1218
1219 // 'Implode' the stack and then hide the dismiss target.
1220 if (touchedView == this) {
1221 mStackAnimationController.implodeStack(
1222 () -> {
1223 mAnimatingMagnet = false;
1224 mShowingDismiss = false;
1225 mDraggingInDismissTarget = false;
1226 after.run();
1227 resetDesaturationAndDarken();
1228 });
1229 } else {
Joshua Tsujif49ee142019-05-29 16:32:01 -04001230 mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001231 mAnimatingMagnet = false;
1232 mShowingDismiss = false;
1233 mDraggingInDismissTarget = false;
1234 resetDesaturationAndDarken();
1235 after.run();
1236 });
1237 }
1238 };
1239
1240 if (mAnimatingMagnet) {
1241 // If the magnet animation is currently playing, dismiss the stack after it's done. This
1242 // happens if the stack is flung towards the target.
1243 mAfterMagnet = animateDismissal;
1244 } else if (mDraggingInDismissTarget) {
1245 // If we're in the dismiss target, but not animating, we already magneted - dismiss
1246 // immediately.
1247 animateDismissal.run();
1248 } else {
1249 // Otherwise, we need to start the magnet animation and then dismiss afterward.
1250 animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY);
1251 mAfterMagnet = animateDismissal;
1252 }
1253 }
1254
1255 /** Animates in the dismiss target, including the gradient behind it. */
1256 private void springInDismissTarget() {
1257 if (mShowingDismiss) {
1258 return;
1259 }
1260
1261 mShowingDismiss = true;
1262
1263 // Show the dismiss container and bring it to the front so the bubbles will go behind it.
1264 mDismissContainer.springIn();
1265 mDismissContainer.bringToFront();
1266 mDismissContainer.setZ(Short.MAX_VALUE - 1);
1267 }
1268
1269 /**
1270 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1271 * were dragged into the target and encircled.
1272 */
1273 private void springOutDismissTargetAndHideCircle() {
1274 if (!mShowingDismiss) {
1275 return;
1276 }
1277
1278 mDismissContainer.springOut();
1279 mShowingDismiss = false;
1280 }
1281
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001282 /** Whether the location of the given MotionEvent is within the dismiss target area. */
Joshua Tsuji6549e702019-05-02 13:13:16 -04001283 boolean isInDismissTarget(MotionEvent ev) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001284 return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001285 }
1286
Joshua Tsuji6549e702019-05-02 13:13:16 -04001287 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1288 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1289 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1290 mFlyoutTransitionSpring
1291 .setStartValue(mFlyoutDragDeltaX)
1292 .setStartVelocity(velX)
1293 .animateToFinalPosition(collapsed
1294 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1295 : 0f);
1296 }
1297
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001298 /**
Mady Mellorfe7ec032019-01-30 17:32:49 -08001299 * Calculates how large the expanded view of the bubble can be. This takes into account the
1300 * y position when the bubbles are expanded as well as the bounds of the dismiss target.
1301 */
1302 int getMaxExpandedHeight() {
1303 int expandedY = (int) mExpandedAnimationController.getExpandedY();
Lyn Han5aa27e22019-05-15 10:55:07 -07001304 // PIP dismiss view uses FLAG_LAYOUT_IN_SCREEN so we need to subtract the bottom inset
1305 int pipDismissHeight = mPipDismissHeight - getBottomInset();
1306 return mDisplaySize.y - expandedY - mBubbleSize - pipDismissHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001307 }
1308
1309 /**
1310 * Calculates the y position of the expanded view when it is expanded.
1311 */
1312 float getYPositionForExpandedView() {
Lyn Han5aa27e22019-05-15 10:55:07 -07001313 return getStatusBarHeight() + mBubbleSize + mBubblePadding + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001314 }
1315
1316 /**
1317 * Called when the height of the currently expanded view has changed (not via an
1318 * update to the bubble's desired height but for some other reason, e.g. permission view
1319 * goes away).
1320 */
1321 void onExpandedHeightChanged() {
1322 if (mIsExpanded) {
1323 requestUpdate();
1324 }
Mady Mellorfe7ec032019-01-30 17:32:49 -08001325 }
1326
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001327 /** Sets whether all bubbles in the stack should not show the 'new' dot. */
1328 void setSuppressNewDot(boolean suppressNewDot) {
1329 mSuppressNewDot = suppressNewDot;
1330
1331 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
1332 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
1333 bv.setSuppressDot(suppressNewDot, true /* animate */);
1334 }
1335 }
1336
1337 /**
1338 * Sets whether the flyout should not appear, even if the notif otherwise would generate one.
1339 */
1340 void setSuppressFlyout(boolean suppressFlyout) {
1341 mSuppressFlyout = suppressFlyout;
1342 }
1343
1344 /**
1345 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
1346 * previous one animates out.
1347 */
1348 private Runnable mAfterFlyoutHides;
1349
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001350 /**
1351 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1352 */
1353 @VisibleForTesting
1354 void animateInFlyoutForBubble(Bubble bubble) {
1355 final CharSequence updateMessage = bubble.entry.getUpdateMessage(getContext());
1356
1357 // Show the message if one exists, and we're not expanded or animating expansion.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001358 if (updateMessage != null
1359 && !isExpanded()
1360 && !mIsExpansionAnimating
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001361 && !mIsGestureInProgress
1362 && !mSuppressFlyout) {
Joshua Tsuji6549e702019-05-02 13:13:16 -04001363 if (bubble.iconView != null) {
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001364 // Temporarily suppress the dot while the flyout is visible.
1365 bubble.iconView.setSuppressDot(
1366 true /* suppressDot */, false /* animate */);
1367
Joshua Tsuji6549e702019-05-02 13:13:16 -04001368 mFlyoutDragDeltaX = 0f;
1369 mFlyout.setAlpha(0f);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001370
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001371 if (mAfterFlyoutHides != null) {
1372 mAfterFlyoutHides.run();
1373 }
1374
1375 mAfterFlyoutHides = () -> {
1376 if (bubble.iconView == null) {
1377 return;
1378 }
1379
1380 // If we're going to suppress the dot, make it visible first so it'll
1381 // visibly animate away.
1382 if (mSuppressNewDot) {
1383 bubble.iconView.setSuppressDot(
1384 false /* suppressDot */, false /* animate */);
1385 }
1386
1387 // Reset dot suppression. If we're not suppressing due to DND, then
1388 // stop suppressing it with no animation (since the flyout has
1389 // transformed into the dot). If we are suppressing due to DND, animate
1390 // it away.
1391 bubble.iconView.setSuppressDot(
1392 mSuppressNewDot /* suppressDot */,
1393 mSuppressNewDot /* animate */);
1394 };
1395
Joshua Tsuji6549e702019-05-02 13:13:16 -04001396 // Post in case layout isn't complete and getWidth returns 0.
Joshua Tsujif49ee142019-05-29 16:32:01 -04001397 post(() -> {
1398 // An auto-expanding bubble could have been posted during the time it takes to
1399 // layout.
1400 if (isExpanded()) {
1401 return;
1402 }
1403
1404 mFlyout.showFlyout(
1405 updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
1406 mStackAnimationController.isStackOnLeftSide(),
1407 bubble.iconView.getBadgeColor(), mAfterFlyoutHides);
1408 });
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001409 }
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001410
Joshua Tsuji6549e702019-05-02 13:13:16 -04001411 mFlyout.removeCallbacks(mHideFlyout);
1412 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Steven Wu1684f2d2019-04-11 14:10:42 -04001413 logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001414 }
1415 }
1416
1417 /** Hide the flyout immediately and cancel any pending hide runnables. */
1418 private void hideFlyoutImmediate() {
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001419 if (mAfterFlyoutHides != null) {
1420 mAfterFlyoutHides.run();
1421 }
1422
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001423 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001424 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001425 }
1426
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001427 @Override
1428 public void getBoundsOnScreen(Rect outRect) {
1429 if (!mIsExpanded) {
Mady Mellor217b2e92019-02-27 11:44:16 -08001430 if (mBubbleContainer.getChildCount() > 0) {
1431 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1432 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001433 } else {
1434 mBubbleContainer.getBoundsOnScreen(outRect);
1435 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001436
Joshua Tsuji6549e702019-05-02 13:13:16 -04001437 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001438 final Rect flyoutBounds = new Rect();
1439 mFlyout.getBoundsOnScreen(flyoutBounds);
1440 outRect.union(flyoutBounds);
1441 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001442 }
1443
1444 private int getStatusBarHeight() {
1445 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001446 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001447 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05001448 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001449 insets.getDisplayCutout() != null
1450 ? insets.getDisplayCutout().getSafeInsetTop()
1451 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001452 }
1453
1454 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001455 }
1456
Mady Mellorfe7ec032019-01-30 17:32:49 -08001457 private int getBottomInset() {
1458 if (getRootWindowInsets() != null) {
1459 WindowInsets insets = getRootWindowInsets();
1460 return insets.getSystemWindowInsetBottom();
1461 }
1462 return 0;
1463 }
1464
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001465 private boolean isIntersecting(View view, float x, float y) {
1466 mTempLoc = view.getLocationOnScreen();
1467 mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
1468 mTempLoc[1] + view.getHeight());
1469 return mTempRect.contains(x, y);
1470 }
1471
1472 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07001473 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001474 return;
1475 }
1476 mViewUpdatedRequested = true;
1477 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1478 invalidate();
1479 }
1480
1481 private void updateExpandedBubble() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001482 if (DEBUG) {
1483 Log.d(TAG, "updateExpandedBubble()");
1484 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001485 mExpandedViewContainer.removeAllViews();
1486 if (mExpandedBubble != null && mIsExpanded) {
1487 mExpandedViewContainer.addView(mExpandedBubble.expandedView);
Mady Mellor5029fa62019-03-05 12:16:21 -08001488 mExpandedBubble.expandedView.populateExpandedView();
Mady Mellor3dff9e62019-02-05 18:12:53 -08001489 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Issei Suzukic0387542019-03-08 17:31:14 +01001490 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001491 }
1492 }
1493
1494 private void applyCurrentState() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001495 if (DEBUG) {
1496 Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded);
1497 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001498
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001499 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08001500 if (mIsExpanded) {
Mady Mellor5029fa62019-03-05 12:16:21 -08001501 // First update the view so that it calculates a new height (ensuring the y position
1502 // calculation is correct)
1503 mExpandedBubble.expandedView.updateView();
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001504 final float y = getYPositionForExpandedView();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001505 if (!mExpandedViewYAnim.isRunning()) {
1506 // We're not animating so set the value
1507 mExpandedViewContainer.setTranslationY(y);
Mady Mellorbc078c22019-03-26 17:10:34 -07001508 mExpandedBubble.expandedView.updateView();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001509 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07001510 // We are animating so update the value; there is an end listener on the animator
1511 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08001512 mExpandedViewYAnim.animateToFinalPosition(y);
1513 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001514 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001515
Joshua Tsuji6549e702019-05-02 13:13:16 -04001516 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
1517 updateBubbleShadowsAndDotPosition(false);
1518 }
1519
1520 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
1521 private void updateBubbleShadowsAndDotPosition(boolean animate) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001522 int bubbsCount = mBubbleContainer.getChildCount();
1523 for (int i = 0; i < bubbsCount; i++) {
1524 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001525 bv.updateDotVisibility(true /* animate */);
Joshua Tsuji25a4b7b2019-03-22 14:11:06 -04001526 bv.setZ((BubbleController.MAX_BUBBLES
1527 * getResources().getDimensionPixelSize(R.dimen.bubble_elevation)) - i);
Joshua Tsuji580c0bf2019-01-28 13:28:21 -05001528
1529 // Draw the shadow around the circle inscribed within the bubble's bounds. This
1530 // (intentionally) does not draw a shadow behind the update dot, which should be drawing
1531 // its own shadow since it's on a different (higher) plane.
1532 bv.setOutlineProvider(new ViewOutlineProvider() {
1533 @Override
1534 public void getOutline(View view, Outline outline) {
1535 outline.setOval(0, 0, mBubbleSize, mBubbleSize);
1536 }
1537 });
1538 bv.setClipToOutline(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001539
1540 // If the dot is on the left, and so is the stack, we need to change the dot position.
1541 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
1542 bv.setDotPosition(!mStackOnLeftOrWillBe, animate);
1543 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001544 }
1545 }
1546
Mady Mellorde2d4d22019-01-29 14:15:34 -08001547 private void updatePointerPosition() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001548 if (DEBUG) {
1549 Log.d(TAG, "updatePointerPosition()");
1550 }
Lyn Han6f6b3ae2019-05-16 14:17:30 -07001551
Lyn Han522e9ff2019-05-17 13:26:13 -07001552 Bubble expandedBubble = getExpandedBubble();
1553 if (expandedBubble == null) {
1554 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08001555 }
Lyn Han522e9ff2019-05-17 13:26:13 -07001556
1557 int index = getBubbleIndex(expandedBubble);
1558 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1559 float halfBubble = mBubbleSize / 2f;
1560
1561 // Bubbles live in expanded view container (x includes expanded view padding).
1562 // Pointer lives in expanded view, which has padding (x does not include padding).
1563 // Remove padding when deriving pointer location from bubbles.
1564 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble - mExpandedViewPadding;
1565
1566 expandedBubble.expandedView.setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08001567 }
1568
Steven Wua254dab2019-01-29 11:30:39 -05001569 /**
1570 * @return the number of bubbles in the stack view.
1571 */
Steven Wub00225b2019-02-08 14:27:42 -05001572 public int getBubbleCount() {
Steven Wua254dab2019-01-29 11:30:39 -05001573 return mBubbleContainer.getChildCount();
1574 }
1575
1576 /**
1577 * Finds the bubble index within the stack.
1578 *
Mady Mellor3dff9e62019-02-05 18:12:53 -08001579 * @param bubble the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05001580 * @return the index of the bubble view within the bubble stack. The range of the position
1581 * is between 0 and the bubble count minus 1.
1582 */
Steven Wua62cb6a2019-02-15 17:12:51 -05001583 int getBubbleIndex(@Nullable Bubble bubble) {
1584 if (bubble == null) {
1585 return 0;
1586 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001587 return mBubbleContainer.indexOfChild(bubble.iconView);
Steven Wua254dab2019-01-29 11:30:39 -05001588 }
1589
1590 /**
1591 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
1592 */
Steven Wub00225b2019-02-08 14:27:42 -05001593 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001594 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05001595 .setScale(4, RoundingMode.CEILING.HALF_UP)
1596 .floatValue();
1597 }
1598
1599 /**
1600 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
1601 */
Steven Wub00225b2019-02-08 14:27:42 -05001602 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001603 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05001604 .setScale(4, RoundingMode.CEILING.HALF_UP)
1605 .floatValue();
1606 }
1607
Joshua Tsujia19515f2019-02-13 18:02:29 -05001608 public PointF getStackPosition() {
1609 return mStackAnimationController.getStackPosition();
1610 }
1611
Steven Wua254dab2019-01-29 11:30:39 -05001612 /**
1613 * Logs the bubble UI event.
1614 *
1615 * @param bubble the bubble that is being interacted on. Null value indicates that
1616 * the user interaction is not specific to one bubble.
1617 * @param action the user interaction enum.
1618 */
Mady Mellor3dff9e62019-02-05 18:12:53 -08001619 private void logBubbleEvent(@Nullable Bubble bubble, int action) {
Steven Wu1684f2d2019-04-11 14:10:42 -04001620 if (bubble == null || bubble.entry == null
1621 || bubble.entry.notification == null) {
Steven Wua254dab2019-01-29 11:30:39 -05001622 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
1623 null /* package name */,
1624 null /* notification channel */,
1625 0 /* notification ID */,
1626 0 /* bubble position */,
1627 getBubbleCount(),
1628 action,
1629 getNormalizedXPosition(),
Steven Wu45e38ae2019-03-25 16:16:59 -04001630 getNormalizedYPosition(),
Steven Wu8ba8ca92019-04-11 10:47:42 -04001631 false /* unread bubble */,
1632 false /* on-going bubble */,
1633 false /* foreground bubble */);
Steven Wua254dab2019-01-29 11:30:39 -05001634 } else {
Mady Mellor3dff9e62019-02-05 18:12:53 -08001635 StatusBarNotification notification = bubble.entry.notification;
Steven Wua254dab2019-01-29 11:30:39 -05001636 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
1637 notification.getPackageName(),
1638 notification.getNotification().getChannelId(),
1639 notification.getId(),
1640 getBubbleIndex(bubble),
1641 getBubbleCount(),
1642 action,
1643 getNormalizedXPosition(),
Steven Wu45e38ae2019-03-25 16:16:59 -04001644 getNormalizedYPosition(),
Steven Wu8ba8ca92019-04-11 10:47:42 -04001645 bubble.entry.showInShadeWhenBubble(),
1646 bubble.entry.isForegroundService(),
1647 BubbleController.isForegroundApp(mContext, notification.getPackageName()));
Steven Wua254dab2019-01-29 11:30:39 -05001648 }
1649 }
Mark Renouf041d7262019-02-06 12:09:41 -05001650
1651 /**
1652 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
1653 * a back key down/up event pair is forwarded to the bubble Activity.
1654 */
1655 boolean performBackPressIfNeeded() {
1656 if (!isExpanded()) {
1657 return false;
1658 }
1659 return mExpandedBubble.expandedView.performBackPressIfNeeded();
1660 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001661
1662 /** For debugging only */
1663 List<Bubble> getBubblesOnScreen() {
1664 List<Bubble> bubbles = new ArrayList<>();
1665 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
1666 View child = mBubbleContainer.getChildAt(i);
1667 if (child instanceof BubbleView) {
1668 String key = ((BubbleView) child).getKey();
1669 Bubble bubble = mBubbleData.getBubbleWithKey(key);
1670 bubbles.add(bubble);
1671 }
1672 }
1673 return bubbles;
1674 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001675}