blob: e666fb5f1af92703b10987aea8b01d9830154eee [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
Mady Mellor5a3e94b2020-02-07 12:16:21 -080022import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
23import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION;
24import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION;
Mady Mellorb8aaf972019-11-26 10:28:00 -080025import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
26import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
Issei Suzukia8d07312019-06-07 12:56:19 +020027import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080028import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
Issei Suzukia8d07312019-06-07 12:56:19 +020029import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
30import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
31
Joshua Tsuji4accf5982019-04-22 17:36:11 -040032import android.animation.Animator;
33import android.animation.AnimatorListenerAdapter;
34import android.animation.ValueAnimator;
Lyn Han6c40fe72019-05-08 14:06:33 -070035import android.app.Notification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036import android.content.Context;
Lyn Hanf4730312019-06-18 11:18:58 -070037import android.content.res.Configuration;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080038import android.content.res.Resources;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080039import android.content.res.TypedArray;
40import android.graphics.Color;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040041import android.graphics.ColorMatrix;
42import android.graphics.ColorMatrixColorFilter;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040043import android.graphics.Paint;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080044import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080045import android.graphics.PointF;
46import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080047import android.graphics.RectF;
Mady Mellor217b2e92019-02-27 11:44:16 -080048import android.os.Bundle;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040049import android.os.Vibrator;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050050import android.util.Log;
Issei Suzukic0387542019-03-08 17:31:14 +010051import android.view.Choreographer;
Mady Mellor9be3bed2019-08-21 17:26:26 -070052import android.view.DisplayCutout;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -040053import android.view.Gravity;
Mady Mellordea7ecf2018-12-10 15:47:40 -080054import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080055import android.view.MotionEvent;
56import android.view.View;
Joshua Tsuji20103542020-02-18 14:06:28 -050057import android.view.ViewGroup;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080058import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050059import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080060import android.view.WindowManager;
Mady Mellor217b2e92019-02-27 11:44:16 -080061import android.view.accessibility.AccessibilityNodeInfo;
Lyn Hane68d0912019-05-02 18:28:01 -070062import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040063import android.view.animation.AccelerateDecelerateInterpolator;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080064import android.widget.FrameLayout;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080065import android.widget.TextView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080066
Mark Renoufcecc77b2019-01-30 16:32:24 -050067import androidx.annotation.MainThread;
Joshua Tsuji20103542020-02-18 14:06:28 -050068import androidx.annotation.NonNull;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080069import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080070import androidx.dynamicanimation.animation.DynamicAnimation;
Joshua Tsuji6549e702019-05-02 13:13:16 -040071import androidx.dynamicanimation.animation.FloatPropertyCompat;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080072import androidx.dynamicanimation.animation.SpringAnimation;
73import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080074
Mady Melloredd4ee12019-01-18 10:45:11 -080075import com.android.internal.annotations.VisibleForTesting;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080076import com.android.internal.util.ContrastColorUtil;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080077import com.android.internal.widget.ViewClippingUtil;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080078import com.android.systemui.Prefs;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080079import com.android.systemui.R;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080080import com.android.systemui.bubbles.animation.ExpandedAnimationController;
81import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
82import com.android.systemui.bubbles.animation.StackAnimationController;
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -080083import com.android.systemui.shared.system.SysUiStatsLog;
Joshua Tsuji20103542020-02-18 14:06:28 -050084import com.android.systemui.util.DismissCircleView;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050085import com.android.systemui.util.FloatingContentCoordinator;
Joshua Tsuji20103542020-02-18 14:06:28 -050086import com.android.systemui.util.animation.PhysicsAnimator;
87import com.android.systemui.util.magnetictarget.MagnetizedObject;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080088
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040089import java.io.FileDescriptor;
90import java.io.PrintWriter;
Steven Wua254dab2019-01-29 11:30:39 -050091import java.math.BigDecimal;
92import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040093import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -040094import java.util.Collections;
95import java.util.List;
Steven Wua254dab2019-01-29 11:30:39 -050096
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080097/**
98 * Renders bubbles in a stack and handles animating expanded and collapsed states.
99 */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500100public class BubbleStackView extends FrameLayout {
Issei Suzukia8d07312019-06-07 12:56:19 +0200101 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800102
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800103 /** Animation durations for bubble stack user education views. **/
104 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
105 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
106
Joshua Tsuji6549e702019-05-02 13:13:16 -0400107 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
108 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
109
110 /** Velocity required to dismiss the flyout via drag. */
111 private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
112
113 /**
114 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
115 * for every 8 pixels overscrolled).
116 */
117 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
118
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400119 /** Duration of the flyout alpha animations. */
120 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
121
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400122 /** Percent to darken the bubbles when they're in the dismiss target. */
123 private static final float DARKEN_PERCENT = 0.3f;
124
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400125 /** How long to wait, in milliseconds, before hiding the flyout. */
126 @VisibleForTesting
127 static final int FLYOUT_HIDE_AFTER = 5000;
128
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400129 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
130 new PhysicsAnimator.SpringConfig(
131 StackAnimationController.IME_ANIMATION_STIFFNESS,
132 StackAnimationController.DEFAULT_BOUNCINESS);
133
Issei Suzukic0387542019-03-08 17:31:14 +0100134 /**
135 * Interface to synchronize {@link View} state and the screen.
136 *
137 * {@hide}
138 */
139 interface SurfaceSynchronizer {
140 /**
141 * Wait until requested change on a {@link View} is reflected on the screen.
142 *
143 * @param callback callback to run after the change is reflected on the screen.
144 */
145 void syncSurfaceAndRun(Runnable callback);
146 }
147
148 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
149 new SurfaceSynchronizer() {
150 @Override
151 public void syncSurfaceAndRun(Runnable callback) {
152 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
153 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
154 // the requested change is reflected on the screen.
155 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
156 // surfaces, rewrite this logic with them.
157 private int mFrameWait = 2;
158
159 @Override
160 public void doFrame(long frameTimeNanos) {
161 if (--mFrameWait > 0) {
162 Choreographer.getInstance().postFrameCallback(this);
163 } else {
164 callback.run();
165 }
166 }
167 });
168 }
169 };
170
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800171 private Point mDisplaySize;
172
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800173 private final SpringAnimation mExpandedViewXAnim;
174 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800175 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800176
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400177 private final Vibrator mVibrator;
178 private final ValueAnimator mDesaturateAndDarkenAnimator;
179 private final Paint mDesaturateAndDarkenPaint = new Paint();
180
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800181 private PhysicsAnimationLayout mBubbleContainer;
182 private StackAnimationController mStackAnimationController;
183 private ExpandedAnimationController mExpandedAnimationController;
184
Mady Mellor3dff9e62019-02-05 18:12:53 -0800185 private FrameLayout mExpandedViewContainer;
186
Joshua Tsuji6549e702019-05-02 13:13:16 -0400187 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400188 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400189 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700190 /**
191 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
192 * previous one animates out.
193 */
Mady Mellorb8aaf972019-11-26 10:28:00 -0800194 private Runnable mAfterFlyoutHidden;
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800195 /**
196 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
197 * once it collapses.
198 */
199 @Nullable
200 private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400201
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400202 /** Layout change listener that moves the stack to the nearest valid position on rotation. */
Lyn Hanf4730312019-06-18 11:18:58 -0700203 private OnLayoutChangeListener mOrientationChangedListener;
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400204 /** Whether the stack was on the left side of the screen prior to rotation. */
205 private boolean mWasOnLeftBeforeRotation = false;
206 /**
207 * How far down the screen the stack was before rotation, in terms of percentage of the way down
208 * the allowable region. Defaults to -1 if not set.
209 */
210 private float mVerticalPosPercentBeforeRotation = -1;
211
Mady Mellor70958542019-09-24 17:12:46 -0700212 private int mMaxBubbles;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800213 private int mBubbleSize;
Mady Mellor70958542019-09-24 17:12:46 -0700214 private int mBubbleElevation;
Lyn Han4a8efe32019-05-30 09:43:27 -0700215 private int mBubblePaddingTop;
Mady Mellore9371bc2019-07-10 18:50:59 -0700216 private int mBubbleTouchPadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700217 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800218 private int mExpandedAnimateXDistance;
219 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700220 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500221 private int mStatusBarHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500222 private int mImeOffset;
Lyn Han3cd75d72020-02-15 19:10:12 -0800223 private BubbleViewProvider mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800224 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800225
Joshua Tsuji6549e702019-05-02 13:13:16 -0400226 /** Whether the stack is currently on the left side of the screen, or animating there. */
227 private boolean mStackOnLeftOrWillBe = false;
228
229 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
230 private boolean mIsGestureInProgress = false;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400231
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400232 /** Description of current animation controller state. */
233 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
234 pw.println("Stack view state:");
235 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
236 pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
237 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400238 mStackAnimationController.dump(fd, pw, args);
239 mExpandedAnimationController.dump(fd, pw, args);
240 }
241
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800242 private BubbleTouchHandler mTouchHandler;
Mady Mellorcd9b1302018-11-06 18:08:04 -0800243 private BubbleController.BubbleExpandListener mExpandListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800244
245 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700246 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400247 private boolean mShowingDismiss = false;
248
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400249 /** The view to desaturate/darken when magneted to the dismiss target. */
250 private View mDesaturateAndDarkenTargetView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800251
Mady Mellor3dff9e62019-02-05 18:12:53 -0800252 private LayoutInflater mInflater;
253
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800254 // Used for determining view / touch intersection
255 int[] mTempLoc = new int[2];
256 RectF mTempRect = new RectF();
257
Mark Renouf821e6782019-04-01 14:17:37 -0400258 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
259
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800260 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
261 new ViewTreeObserver.OnPreDrawListener() {
262 @Override
263 public boolean onPreDraw() {
264 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Lyn Han285ad302019-05-29 19:01:39 -0700265 updateExpandedView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800266 mViewUpdatedRequested = false;
267 return true;
268 }
269 };
270
Mark Renouf821e6782019-04-01 14:17:37 -0400271 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
272 this::updateSystemGestureExcludeRects;
273
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800274 private ViewClippingUtil.ClippingParameters mClippingParameters =
275 new ViewClippingUtil.ClippingParameters() {
276
Lyn Han522e9ff2019-05-17 13:26:13 -0700277 @Override
278 public boolean shouldFinish(View view) {
279 return false;
280 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800281
Lyn Han522e9ff2019-05-17 13:26:13 -0700282 @Override
283 public boolean isClippingEnablingAllowed(View view) {
284 return !mIsExpanded;
285 }
286 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800287
Joshua Tsuji6549e702019-05-02 13:13:16 -0400288 /** Float property that 'drags' the flyout. */
289 private final FloatPropertyCompat mFlyoutCollapseProperty =
290 new FloatPropertyCompat("FlyoutCollapseSpring") {
291 @Override
292 public float getValue(Object o) {
293 return mFlyoutDragDeltaX;
294 }
295
296 @Override
297 public void setValue(Object o, float v) {
298 onFlyoutDragged(v);
299 }
300 };
301
302 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
303 private final SpringAnimation mFlyoutTransitionSpring =
304 new SpringAnimation(this, mFlyoutCollapseProperty);
305
306 /** Distance the flyout has been dragged in the X axis. */
307 private float mFlyoutDragDeltaX = 0f;
308
309 /**
Joshua Tsuji14e68552019-06-06 17:17:08 -0400310 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
311 */
312 private Runnable mAnimateInFlyout;
313
314 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -0400315 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
316 * it immediately.
317 */
318 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
319 (dynamicAnimation, b, v, v1) -> {
320 if (mFlyoutDragDeltaX == 0) {
321 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
322 } else {
323 mFlyout.hideFlyout();
324 }
325 };
326
Lyn Han1b4f25e2019-06-11 13:56:34 -0700327 @NonNull
328 private final SurfaceSynchronizer mSurfaceSynchronizer;
Issei Suzukic0387542019-03-08 17:31:14 +0100329
Joshua Tsuji20103542020-02-18 14:06:28 -0500330 /**
331 * The currently magnetized object, which is being dragged and will be attracted to the magnetic
332 * dismiss target.
333 *
334 * This is either the stack itself, or an individual bubble.
335 */
336 private MagnetizedObject<?> mMagnetizedObject;
337
338 /**
339 * The action to run when the magnetized object is released in the dismiss target.
340 *
341 * This will actually perform the dismissal of either the stack or an individual bubble.
342 */
343 private Runnable mReleasedInDismissTargetAction;
344
345 /**
346 * The MagneticTarget instance for our circular dismiss view. This is added to the
347 * MagnetizedObject instances for the stack and any dragged-out bubbles.
348 */
349 private MagnetizedObject.MagneticTarget mMagneticTarget;
350
351 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
352 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
353 new MagnetizedObject.MagnetListener() {
354 @Override
355 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
356 animateDesaturateAndDarken(
357 mExpandedAnimationController.getDraggedOutBubble(), true);
358 }
359
360 @Override
361 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
362 float velX, float velY, boolean wasFlungOut) {
363 animateDesaturateAndDarken(
364 mExpandedAnimationController.getDraggedOutBubble(), false);
365
366 if (wasFlungOut) {
367 mExpandedAnimationController.snapBubbleBack(
368 mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
369 hideDismissTarget();
370 } else {
371 mExpandedAnimationController.onUnstuckFromTarget();
372 }
373 }
374
375 @Override
376 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
377 mExpandedAnimationController.dismissDraggedOutBubble(
378 mExpandedAnimationController.getDraggedOutBubble(),
379 mReleasedInDismissTargetAction);
380 hideDismissTarget();
381 }
382 };
383
384 /** Magnet listener that handles animating and dismissing the entire stack. */
385 private final MagnetizedObject.MagnetListener mStackMagnetListener =
386 new MagnetizedObject.MagnetListener() {
387 @Override
388 public void onStuckToTarget(
389 @NonNull MagnetizedObject.MagneticTarget target) {
390 animateDesaturateAndDarken(mBubbleContainer, true);
391 }
392
393 @Override
394 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
395 float velX, float velY, boolean wasFlungOut) {
396 animateDesaturateAndDarken(mBubbleContainer, false);
397
398 if (wasFlungOut) {
399 mStackAnimationController.flingStackThenSpringToEdge(
400 mStackAnimationController.getStackPosition().x, velX, velY);
401 hideDismissTarget();
402 } else {
403 mStackAnimationController.onUnstuckFromTarget();
404 }
405 }
406
407 @Override
408 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
409 mStackAnimationController.implodeStack(
410 () -> {
411 resetDesaturationAndDarken();
412 mReleasedInDismissTargetAction.run();
413 }
414 );
415
416 hideDismissTarget();
417 }
418 };
419
420 private ViewGroup mDismissTargetContainer;
421 private PhysicsAnimator<View> mDismissTargetAnimator;
422 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
423 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Issei Suzukic0387542019-03-08 17:31:14 +0100424
Lyn Hanf4730312019-06-18 11:18:58 -0700425 private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
Lyn Han3cd75d72020-02-15 19:10:12 -0800426
Joshua Tsujia2433db2020-03-12 17:56:22 -0400427 @Nullable
Lyn Han3cd75d72020-02-15 19:10:12 -0800428 private BubbleOverflow mBubbleOverflow;
Lyn Hanf4730312019-06-18 11:18:58 -0700429
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800430 private boolean mShouldShowUserEducation;
431 private boolean mAnimatingEducationAway;
432 private View mUserEducationView;
433
434 private boolean mShouldShowManageEducation;
435 private BubbleManageEducationView mManageEducationView;
436 private boolean mAnimatingManageEducationAway;
437
Issei Suzukic0387542019-03-08 17:31:14 +0100438 public BubbleStackView(Context context, BubbleData data,
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500439 @Nullable SurfaceSynchronizer synchronizer,
440 FloatingContentCoordinator floatingContentCoordinator) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800441 super(context);
442
Mady Mellorcfd06c12019-02-13 14:32:12 -0800443 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800444 mInflater = LayoutInflater.from(context);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400445 mTouchHandler = new BubbleTouchHandler(this, data, context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800446 setOnTouchListener(mTouchHandler);
447
448 Resources res = getResources();
Mady Mellor70958542019-09-24 17:12:46 -0700449 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800450 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellor70958542019-09-24 17:12:46 -0700451 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Lyn Han4a8efe32019-05-30 09:43:27 -0700452 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellore9371bc2019-07-10 18:50:59 -0700453 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800454 mExpandedAnimateXDistance =
455 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
456 mExpandedAnimateYDistance =
457 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700458 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
459
Joshua Tsujif44347f2019-02-12 14:28:06 -0500460 mStatusBarHeight =
461 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500462 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800463
464 mDisplaySize = new Point();
465 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700466 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700467 wm.getDefaultDisplay().getRealSize(mDisplaySize);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800468
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400469 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
470
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700471 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800472 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800473
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400474 mStackAnimationController = new StackAnimationController(
475 floatingContentCoordinator, this::getBubbleCount);
Lyn Hanf4730312019-06-18 11:18:58 -0700476
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700477 mExpandedAnimationController = new ExpandedAnimationController(
Lyn Hanf4730312019-06-18 11:18:58 -0700478 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
Issei Suzukic0387542019-03-08 17:31:14 +0100479 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800480
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800481 setUpUserEducation();
482
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800483 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400484 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800485 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800486 mBubbleContainer.setClipChildren(false);
487 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
488
Mady Mellor3dff9e62019-02-05 18:12:53 -0800489 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800490 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700491 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
492 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800493 mExpandedViewContainer.setClipChildren(false);
494 addView(mExpandedViewContainer);
495
Mady Mellor8bfe5412019-07-31 14:56:44 -0700496 setUpFlyout();
Joshua Tsuji6549e702019-05-02 13:13:16 -0400497 mFlyoutTransitionSpring.setSpring(new SpringForce()
Joshua Tsuji14e68552019-06-06 17:17:08 -0400498 .setStiffness(SpringForce.STIFFNESS_LOW)
Joshua Tsuji6549e702019-05-02 13:13:16 -0400499 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
500 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
501
Joshua Tsujie48c4112020-02-26 14:36:25 -0500502 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
Joshua Tsuji20103542020-02-18 14:06:28 -0500503 final View targetView = new DismissCircleView(context);
504 final FrameLayout.LayoutParams newParams =
Joshua Tsujie48c4112020-02-26 14:36:25 -0500505 new FrameLayout.LayoutParams(targetSize, targetSize);
Joshua Tsuji20103542020-02-18 14:06:28 -0500506 newParams.gravity = Gravity.CENTER;
507 targetView.setLayoutParams(newParams);
508 mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
509
510 mDismissTargetContainer = new FrameLayout(context);
511 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
Joshua Tsuji6549e702019-05-02 13:13:16 -0400512 MATCH_PARENT,
513 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
514 Gravity.BOTTOM));
Joshua Tsuji20103542020-02-18 14:06:28 -0500515 mDismissTargetContainer.setClipChildren(false);
516 mDismissTargetContainer.addView(targetView);
517 mDismissTargetContainer.setVisibility(View.INVISIBLE);
518 addView(mDismissTargetContainer);
519
520 // Start translated down so the target springs up.
521 targetView.setTranslationY(
522 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height));
523
524 // Save the MagneticTarget instance for the newly set up view - we'll add this to the
525 // MagnetizedObjects.
526 mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400527
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800528 mExpandedViewXAnim =
529 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
530 mExpandedViewXAnim.setSpring(
531 new SpringForce()
532 .setStiffness(SpringForce.STIFFNESS_LOW)
533 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
534
535 mExpandedViewYAnim =
536 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
537 mExpandedViewYAnim.setSpring(
538 new SpringForce()
539 .setStiffness(SpringForce.STIFFNESS_LOW)
540 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorbc078c22019-03-26 17:10:34 -0700541 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
Lyn Han3cd75d72020-02-15 19:10:12 -0800542 if (mIsExpanded && mExpandedBubble != null) {
543 mExpandedBubble.getExpandedView().updateView();
Mady Mellorbc078c22019-03-26 17:10:34 -0700544 }
545 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800546
547 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800548 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500549 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800550
Lyn Hancd4f87e2020-02-19 20:33:45 -0800551 setUpOverflow();
Lyn Hanb58c7562020-01-07 14:29:20 -0800552
Mady Mellor5d8f1402019-02-21 18:23:52 -0800553 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
Mady Mellorbc078c22019-03-26 17:10:34 -0700554 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800555 return view.onApplyWindowInsets(insets);
556 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800557 mExpandedAnimationController.updateYPosition(
558 // Update the insets after we're done translating otherwise position
559 // calculation for them won't be correct.
Lyn Hanb58c7562020-01-07 14:29:20 -0800560 () -> {
Lyn Han3cd75d72020-02-15 19:10:12 -0800561 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800562 mExpandedBubble.getExpandedView().updateInsets(insets);
563 }
564 });
Mady Mellor5d8f1402019-02-21 18:23:52 -0800565 return view.onApplyWindowInsets(insets);
566 });
Mark Renouf821e6782019-04-01 14:17:37 -0400567
Lyn Hanf4730312019-06-18 11:18:58 -0700568 mOrientationChangedListener =
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400569 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
Mady Mellor9be3bed2019-08-21 17:26:26 -0700570 mExpandedAnimationController.updateOrientation(mOrientation, mDisplaySize);
571 mStackAnimationController.updateOrientation(mOrientation);
572
573 // Reposition & adjust the height for new orientation
574 if (mIsExpanded) {
575 mExpandedViewContainer.setTranslationY(getExpandedViewY());
Lyn Han3cd75d72020-02-15 19:10:12 -0800576 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800577 mExpandedBubble.getExpandedView().updateView();
578 }
Mady Mellor9be3bed2019-08-21 17:26:26 -0700579 }
580
581 // Need to update the padding around the view
582 WindowInsets insets = getRootWindowInsets();
583 int leftPadding = mExpandedViewPadding;
584 int rightPadding = mExpandedViewPadding;
585 if (insets != null) {
586 // Can't have the expanded view overlaying notches
587 int cutoutLeft = 0;
588 int cutoutRight = 0;
589 DisplayCutout cutout = insets.getDisplayCutout();
590 if (cutout != null) {
591 cutoutLeft = cutout.getSafeInsetLeft();
592 cutoutRight = cutout.getSafeInsetRight();
593 }
594 // Or overlaying nav or status bar
595 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
596 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
597 }
598 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
599 rightPadding, mExpandedViewPadding);
600
Lyn Hanf4730312019-06-18 11:18:58 -0700601 if (mIsExpanded) {
602 // Re-draw bubble row and pointer for new orientation.
603 mExpandedAnimationController.expandFromStack(() -> {
604 updatePointerPosition();
605 } /* after */);
606 }
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400607 if (mVerticalPosPercentBeforeRotation >= 0) {
608 mStackAnimationController.moveStackToSimilarPositionAfterRotation(
609 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
610 }
Lyn Hanf4730312019-06-18 11:18:58 -0700611 removeOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400612 };
613
Mark Renouf821e6782019-04-01 14:17:37 -0400614 // This must be a separate OnDrawListener since it should be called for every draw.
615 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400616
617 final ColorMatrix animatedMatrix = new ColorMatrix();
618 final ColorMatrix darkenMatrix = new ColorMatrix();
619
620 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
621 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
622 final float animatedValue = (float) animation.getAnimatedValue();
623 animatedMatrix.setSaturation(animatedValue);
624
625 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
626 darkenMatrix.setScale(
627 1f - animatedDarkenValue /* red */,
628 1f - animatedDarkenValue /* green */,
629 1f - animatedDarkenValue /* blue */,
630 1f /* alpha */);
631
632 // Concat the matrices so that the animatedMatrix both desaturates and darkens.
633 animatedMatrix.postConcat(darkenMatrix);
634
635 // Update the paint and apply it to the bubble container.
636 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
637 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
638 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800639 }
640
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800641 private void setUpUserEducation() {
642 if (mUserEducationView != null) {
643 removeView(mUserEducationView);
644 }
645 mShouldShowUserEducation = shouldShowBubblesEducation();
646 if (DEBUG_USER_EDUCATION) {
647 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
648 }
649 if (mShouldShowUserEducation) {
650 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
651 false /* attachToRoot */);
652 mUserEducationView.setVisibility(GONE);
653
654 final TypedArray ta = mContext.obtainStyledAttributes(
655 new int[] {android.R.attr.colorAccent,
656 android.R.attr.textColorPrimaryInverse});
657 final int bgColor = ta.getColor(0, Color.BLACK);
658 int textColor = ta.getColor(1, Color.WHITE);
659 ta.recycle();
660 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
661
662 TextView title = mUserEducationView.findViewById(R.id.user_education_title);
663 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
664 title.setTextColor(textColor);
665 description.setTextColor(textColor);
666
667 addView(mUserEducationView);
668 }
669
670 if (mManageEducationView != null) {
671 removeView(mManageEducationView);
672 }
673 mShouldShowManageEducation = shouldShowManageEducation();
674 if (DEBUG_USER_EDUCATION) {
675 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
676 }
677 if (mShouldShowManageEducation) {
678 mManageEducationView = (BubbleManageEducationView)
679 mInflater.inflate(R.layout.bubbles_manage_button_education, this,
680 false /* attachToRoot */);
681 mManageEducationView.setVisibility(GONE);
682 mManageEducationView.setElevation(mBubbleElevation);
683
684 addView(mManageEducationView);
Lyn Hanb58c7562020-01-07 14:29:20 -0800685 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800686 }
687
Mady Mellor8bfe5412019-07-31 14:56:44 -0700688 private void setUpFlyout() {
689 if (mFlyout != null) {
690 removeView(mFlyout);
691 }
692 mFlyout = new BubbleFlyoutView(getContext());
693 mFlyout.setVisibility(GONE);
694 mFlyout.animate()
695 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
696 .setInterpolator(new AccelerateDecelerateInterpolator());
697 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
698 }
699
Lyn Hancd4f87e2020-02-19 20:33:45 -0800700 private void setUpOverflow() {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800701 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
702 return;
703 }
Lyn Hancd4f87e2020-02-19 20:33:45 -0800704 int overflowBtnIndex = 0;
705 if (mBubbleOverflow == null) {
Mady Mellor0122cc92020-02-27 12:15:39 -0800706 mBubbleOverflow = new BubbleOverflow(getContext());
707 mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800708 } else {
709 mBubbleContainer.removeView(mBubbleOverflow.getBtn());
710 mBubbleOverflow.updateIcon(mContext, this);
711 overflowBtnIndex = mBubbleContainer.getChildCount() - 1;
712 }
713 mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
714 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
715
716 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700717 /**
Lyn Han02cca812019-04-02 16:27:32 -0700718 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700719 */
Lyn Han02cca812019-04-02 16:27:32 -0700720 public void onThemeChanged() {
Mady Mellor8bfe5412019-07-31 14:56:44 -0700721 setUpFlyout();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800722 setUpOverflow();
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800723 setUpUserEducation();
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700724 }
725
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400726 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
Lyn Hanf4730312019-06-18 11:18:58 -0700727 public void onOrientationChanged(int orientation) {
728 mOrientation = orientation;
729
Mady Mellore19353d2019-08-21 17:25:02 -0700730 // Display size is based on the rotation device was in when requested, we should update it
Mady Mellor9be3bed2019-08-21 17:26:26 -0700731 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700732 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
733 wm.getDefaultDisplay().getRealSize(mDisplaySize);
734
Mady Mellor818eef02019-08-16 16:12:29 -0700735 // Some resources change depending on orientation
736 Resources res = getContext().getResources();
737 mStatusBarHeight = res.getDimensionPixelSize(
738 com.android.internal.R.dimen.status_bar_height);
739 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
740
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400741 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
742 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
743 mVerticalPosPercentBeforeRotation =
744 (mStackAnimationController.getStackPosition().y - allowablePos.top)
745 / (allowablePos.bottom - allowablePos.top);
Lyn Hanf4730312019-06-18 11:18:58 -0700746 addOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400747 hideFlyoutImmediate();
748 }
749
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800750 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800751 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
752 getBoundsOnScreen(outRect);
753 }
754
755 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800756 protected void onDetachedFromWindow() {
757 super.onDetachedFromWindow();
758 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
759 }
760
761 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800762 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
763 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Hane68d0912019-05-02 18:28:01 -0700764
765 // Custom actions.
766 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
767 getContext().getResources()
768 .getString(R.string.bubble_accessibility_action_move_top_left));
769 info.addAction(moveTopLeft);
770
771 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
772 getContext().getResources()
773 .getString(R.string.bubble_accessibility_action_move_top_right));
774 info.addAction(moveTopRight);
775
776 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
777 getContext().getResources()
778 .getString(R.string.bubble_accessibility_action_move_bottom_left));
779 info.addAction(moveBottomLeft);
780
781 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
782 getContext().getResources()
783 .getString(R.string.bubble_accessibility_action_move_bottom_right));
784 info.addAction(moveBottomRight);
785
786 // Default actions.
787 info.addAction(AccessibilityAction.ACTION_DISMISS);
Mady Mellor217b2e92019-02-27 11:44:16 -0800788 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -0700789 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -0800790 } else {
Lyn Hane68d0912019-05-02 18:28:01 -0700791 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -0800792 }
793 }
794
795 @Override
796 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
797 if (super.performAccessibilityActionInternal(action, arguments)) {
798 return true;
799 }
Lyn Hane68d0912019-05-02 18:28:01 -0700800 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
801
802 // R constants are not final so we cannot use switch-case here.
803 if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
804 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
805 return true;
806 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
807 mBubbleData.setExpanded(false);
808 return true;
809 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
810 mBubbleData.setExpanded(true);
811 return true;
812 } else if (action == R.id.action_move_top_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500813 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -0700814 return true;
815 } else if (action == R.id.action_move_top_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500816 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -0700817 return true;
818 } else if (action == R.id.action_move_bottom_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500819 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -0700820 return true;
821 } else if (action == R.id.action_move_bottom_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500822 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -0700823 return true;
Mady Mellor217b2e92019-02-27 11:44:16 -0800824 }
825 return false;
826 }
827
Lyn Han6c40fe72019-05-08 14:06:33 -0700828 /**
829 * Update content description for a11y TalkBack.
830 */
831 public void updateContentDescription() {
832 if (mBubbleData.getBubbles().isEmpty()) {
833 return;
834 }
835 Bubble topBubble = mBubbleData.getBubbles().get(0);
836 String appName = topBubble.getAppName();
Ned Burns00b4b2d2019-10-17 22:09:27 -0400837 Notification notification = topBubble.getEntry().getSbn().getNotification();
Lyn Han6c40fe72019-05-08 14:06:33 -0700838 CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
Lyn Han93cd3132020-02-18 18:22:05 -0800839 String titleStr = getResources().getString(R.string.notification_bubble_title);
Lyn Han6c40fe72019-05-08 14:06:33 -0700840 if (titleCharSeq != null) {
841 titleStr = titleCharSeq.toString();
842 }
843 int moreCount = mBubbleContainer.getChildCount() - 1;
844
845 // Example: Title from app name.
846 String singleDescription = getResources().getString(
847 R.string.bubble_content_description_single, titleStr, appName);
848
849 // Example: Title from app name and 4 more.
850 String stackDescription = getResources().getString(
851 R.string.bubble_content_description_stack, titleStr, appName, moreCount);
852
853 if (mIsExpanded) {
854 // TODO(b/129522932) - update content description for each bubble in expanded view.
855 } else {
856 // Collapsed stack.
857 if (moreCount > 0) {
858 mBubbleContainer.setContentDescription(stackDescription);
859 } else {
860 mBubbleContainer.setContentDescription(singleDescription);
861 }
862 }
863 }
864
Mark Renouf821e6782019-04-01 14:17:37 -0400865 private void updateSystemGestureExcludeRects() {
866 // Exclude the region occupied by the first BubbleView in the stack
867 Rect excludeZone = mSystemGestureExclusionRects.get(0);
Lyn Hanc47e1712020-01-28 21:43:34 -0800868 if (getBubbleCount() > 0) {
Mark Renouf821e6782019-04-01 14:17:37 -0400869 View firstBubble = mBubbleContainer.getChildAt(0);
870 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
871 firstBubble.getBottom());
872 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
873 (int) (firstBubble.getTranslationY() + 0.5f));
874 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
875 } else {
876 excludeZone.setEmpty();
877 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
878 }
879 }
880
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800881 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800882 * Sets the listener to notify when the bubble stack is expanded.
883 */
884 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
885 mExpandListener = listener;
886 }
887
888 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800889 * Whether the stack of bubbles is expanded or not.
890 */
891 public boolean isExpanded() {
892 return mIsExpanded;
893 }
894
895 /**
Mady Mellor047e24e2019-08-05 11:35:40 -0700896 * Whether the stack of bubbles is animating to or from expansion.
897 */
898 public boolean isExpansionAnimating() {
899 return mIsExpansionAnimating;
900 }
901
902 /**
Mady Mellorb8aaf972019-11-26 10:28:00 -0800903 * The {@link BadgedImageView} that is expanded, null if one does not exist.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800904 */
Lyn Han3cd75d72020-02-15 19:10:12 -0800905 View getExpandedBubbleView() {
Mady Mellored99c272019-06-13 15:58:30 -0700906 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800907 }
908
909 /**
910 * The {@link Bubble} that is expanded, null if one does not exist.
911 */
Lyn Han3cd75d72020-02-15 19:10:12 -0800912 @Nullable
Lyn Han9f66c3b2020-03-05 23:59:29 -0800913 BubbleViewProvider getExpandedBubble() {
914 return mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800915 }
916
Mark Renouf71a3af62019-04-08 15:02:54 -0400917 // via BubbleData.Listener
918 void addBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200919 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400920 Log.d(TAG, "addBubble: " + bubble);
921 }
Joshua Tsujib35f5912019-07-24 16:15:21 -0400922
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800923 if (getBubbleCount() == 0 && mShouldShowUserEducation) {
924 // Override the default stack position if we're showing user education.
925 mStackAnimationController.setStackPosition(
926 mStackAnimationController.getDefaultStartPosition());
927 }
928
Lyn Hanc47e1712020-01-28 21:43:34 -0800929 if (getBubbleCount() == 0) {
Joshua Tsujib35f5912019-07-24 16:15:21 -0400930 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
931 }
932
Joshua Tsujib35f5912019-07-24 16:15:21 -0400933 // Set the dot position to the opposite of the side the stack is resting on, since the stack
934 // resting slightly off-screen would result in the dot also being off-screen.
935 bubble.getIconView().setDotPosition(
936 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
937
Mady Mellored99c272019-06-13 15:58:30 -0700938 mBubbleContainer.addView(bubble.getIconView(), 0,
Mark Renouf71a3af62019-04-08 15:02:54 -0400939 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellored99c272019-06-13 15:58:30 -0700940 ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
Mark Renoufba5ab512019-05-02 15:21:01 -0400941 animateInFlyoutForBubble(bubble);
Lyn Hanb58c7562020-01-07 14:29:20 -0800942 updatePointerPosition();
943 updateOverflowBtnVisibility( /*apply */ true);
Mark Renouf71a3af62019-04-08 15:02:54 -0400944 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -0800945 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mark Renouf71a3af62019-04-08 15:02:54 -0400946 }
947
948 // via BubbleData.Listener
949 void removeBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +0200950 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400951 Log.d(TAG, "removeBubble: " + bubble);
952 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400953 // Remove it from the views
Mady Mellored99c272019-06-13 15:58:30 -0700954 int removedIndex = mBubbleContainer.indexOfChild(bubble.getIconView());
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400955 if (removedIndex >= 0) {
956 mBubbleContainer.removeViewAt(removedIndex);
Mark Renoufc19b4732019-06-26 12:08:33 -0400957 bubble.cleanupExpandedState();
Lyn Han1e19d7f2020-02-05 19:10:58 -0800958 bubble.setInflated(false);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -0800959 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400960 } else {
961 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
962 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800963 updateOverflowBtnVisibility(/* apply */ true);
964 }
965
966 private void updateOverflowBtnVisibility(boolean apply) {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800967 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
968 return;
969 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800970 if (mIsExpanded) {
971 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800972 Log.d(TAG, "Show overflow button.");
Lyn Hanb58c7562020-01-07 14:29:20 -0800973 }
Lyn Han3cd75d72020-02-15 19:10:12 -0800974 mBubbleOverflow.setBtnVisible(VISIBLE);
Lyn Hanb58c7562020-01-07 14:29:20 -0800975 if (apply) {
Lyn Hanc47e1712020-01-28 21:43:34 -0800976 mExpandedAnimationController.expandFromStack(() -> {
977 updatePointerPosition();
978 } /* after */);
Lyn Hanb58c7562020-01-07 14:29:20 -0800979 }
980 } else {
981 if (DEBUG_BUBBLE_STACK_VIEW) {
982 Log.d(TAG, "Collapsed. Hide overflow button.");
983 }
Lyn Han3cd75d72020-02-15 19:10:12 -0800984 mBubbleOverflow.setBtnVisible(GONE);
Lyn Hanb58c7562020-01-07 14:29:20 -0800985 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400986 }
987
988 // via BubbleData.Listener
989 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -0400990 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -0400991 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -0800992 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mark Renouf71a3af62019-04-08 15:02:54 -0400993 }
994
Mark Renoufba5ab512019-05-02 15:21:01 -0400995 public void updateBubbleOrder(List<Bubble> bubbles) {
996 for (int i = 0; i < bubbles.size(); i++) {
997 Bubble bubble = bubbles.get(i);
Mady Mellored99c272019-06-13 15:58:30 -0700998 mBubbleContainer.reorderView(bubble.getIconView(), i);
Mark Renoufba5ab512019-05-02 15:21:01 -0400999 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001000
1001 updateBubbleZOrdersAndDotPosition(false /* animate */);
Mark Renoufba5ab512019-05-02 15:21:01 -04001002 }
1003
Lyn Han99e457f2020-01-31 10:14:19 -08001004 void showOverflow() {
Lyn Han3cd75d72020-02-15 19:10:12 -08001005 setSelectedBubble(mBubbleOverflow);
Lyn Han99e457f2020-01-31 10:14:19 -08001006 }
1007
Mady Melloredd4ee12019-01-18 10:45:11 -08001008 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001009 * Changes the currently selected bubble. If the stack is already expanded, the newly selected
1010 * bubble will be shown immediately. This does not change the expanded state or change the
1011 * position of any bubble.
1012 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001013 // via BubbleData.Listener
Lyn Han3cd75d72020-02-15 19:10:12 -08001014 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001015 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001016 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
1017 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001018 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
1019 return;
1020 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001021 final BubbleViewProvider previouslySelected = mExpandedBubble;
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001022 mExpandedBubble = bubbleToSelect;
Issei Suzukicac2a502019-04-16 16:52:50 +02001023
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001024 if (mIsExpanded) {
1025 // Make the container of the expanded view transparent before removing the expanded view
1026 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
1027 // expanded view becomes visible on the screen. See b/126856255
1028 mExpandedViewContainer.setAlpha(0.0f);
1029 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Lyn Han3cd75d72020-02-15 19:10:12 -08001030 previouslySelected.setContentVisibility(false);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001031 updateExpandedBubble();
1032 updatePointerPosition();
1033 requestUpdate();
Lyn Han3cd75d72020-02-15 19:10:12 -08001034
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001035 logBubbleEvent(previouslySelected,
1036 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
1037 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor99a302602019-06-14 11:39:56 -07001038 notifyExpansionChanged(previouslySelected, false /* expanded */);
1039 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001040 });
1041 }
1042 }
1043
1044 /**
1045 * Changes the expanded state of the stack.
1046 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001047 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001048 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001049 // via BubbleData.Listener
1050 public void setExpanded(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001051 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001052 Log.d(TAG, "setExpanded: " + shouldExpand);
1053 }
Lyn Han285ad302019-05-29 19:01:39 -07001054 if (shouldExpand == mIsExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001055 return;
1056 }
Lyn Han285ad302019-05-29 19:01:39 -07001057 if (mIsExpanded) {
1058 animateCollapse();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001059 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001060 } else {
Lyn Han285ad302019-05-29 19:01:39 -07001061 animateExpansion();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001062 // TODO: move next line to BubbleData
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001063 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
1064 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001065 }
Mady Mellor99a302602019-06-14 11:39:56 -07001066 notifyExpansionChanged(mExpandedBubble, mIsExpanded);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001067 }
1068
1069 /**
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001070 * If necessary, shows the user education view for the bubble stack. This appears the first
1071 * time a user taps on a bubble.
1072 *
1073 * @return true if user education was shown, false otherwise.
1074 */
1075 private boolean maybeShowStackUserEducation() {
1076 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
1077 Bubble b = mBubbleData.getSelectedBubble();
1078 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
1079 description.setText(mContext.getString(
1080 R.string.bubbles_user_education_description, b.getAppName()));
1081
1082 mUserEducationView.setAlpha(0);
1083 mUserEducationView.setVisibility(VISIBLE);
1084 // Post so we have height of mUserEducationView
1085 mUserEducationView.post(() -> {
1086 final int viewHeight = mUserEducationView.getHeight();
1087 PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
1088 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
1089 mUserEducationView.setTranslationY(translationY);
1090 mUserEducationView.animate()
1091 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1092 .setInterpolator(FAST_OUT_SLOW_IN)
1093 .alpha(1);
1094 });
1095 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
1096 return true;
1097 }
1098 return false;
1099 }
1100
1101 /**
1102 * If necessary, hides the user education view for the bubble stack.
1103 *
1104 * @param fromExpansion if true this indicates the hide is happening due to the bubble being
1105 * expanded, false if due to a touch outside of the bubble stack.
1106 */
1107 void hideStackUserEducation(boolean fromExpansion) {
1108 if (mShouldShowUserEducation
1109 && mUserEducationView.getVisibility() == VISIBLE
1110 && !mAnimatingEducationAway) {
1111 mAnimatingEducationAway = true;
1112 mUserEducationView.animate()
1113 .alpha(0)
1114 .setDuration(fromExpansion
1115 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1116 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1117 .withEndAction(() -> {
1118 mAnimatingEducationAway = false;
1119 mShouldShowUserEducation = shouldShowBubblesEducation();
1120 mUserEducationView.setVisibility(GONE);
1121 });
1122 }
1123 }
1124
1125 /**
1126 * If necessary, toggles the user education view for the manage button. This is shown when the
1127 * bubble stack is expanded for the first time.
1128 *
1129 * @param show whether the user education view should show or not.
1130 */
1131 void maybeShowManageEducation(boolean show) {
1132 if (mManageEducationView == null) {
1133 return;
1134 }
1135 if (show
1136 && mShouldShowManageEducation
1137 && mManageEducationView.getVisibility() != VISIBLE
1138 && mIsExpanded) {
1139 mManageEducationView.setAlpha(0);
1140 mManageEducationView.setVisibility(VISIBLE);
1141 mManageEducationView.post(() -> {
1142 final Rect position =
1143 mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
1144 final int viewHeight = mManageEducationView.getManageViewHeight();
1145 final int inset = getResources().getDimensionPixelSize(
1146 R.dimen.bubbles_manage_education_top_inset);
1147 mManageEducationView.bringToFront();
1148 mManageEducationView.setManageViewPosition(position.left,
1149 position.top - viewHeight + inset);
1150 mManageEducationView.setPointerPosition(position.centerX() - position.left);
1151 mManageEducationView.animate()
1152 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1153 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
1154 });
1155 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
1156 } else if (!show
1157 && mManageEducationView.getVisibility() == VISIBLE
1158 && !mAnimatingManageEducationAway) {
1159 mManageEducationView.animate()
1160 .alpha(0)
1161 .setDuration(mIsExpansionAnimating
1162 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1163 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1164 .withEndAction(() -> {
1165 mAnimatingManageEducationAway = false;
1166 mShouldShowManageEducation = shouldShowManageEducation();
1167 mManageEducationView.setVisibility(GONE);
1168 });
1169 }
1170 }
1171
Joshua Tsuji20103542020-02-18 14:06:28 -05001172 /*
1173 * Sets the action to run to dismiss the currently dragging object (either the stack or an
1174 * individual bubble).
1175 */
1176 public void setReleasedInDismissTargetAction(Runnable action) {
1177 mReleasedInDismissTargetAction = action;
1178 }
1179
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001180 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001181 * Dismiss the stack of bubbles.
Lyn Han1b4f25e2019-06-11 13:56:34 -07001182 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001183 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001184 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001185 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -07001186 void stackDismissed(int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001187 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001188 Log.d(TAG, "stackDismissed: reason=" + reason);
1189 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001190 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -05001191 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001192 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001193 }
1194
1195 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001196 * @return the view the touch event is on
1197 */
1198 @Nullable
1199 public View getTargetView(MotionEvent event) {
1200 float x = event.getRawX();
1201 float y = event.getRawY();
1202 if (mIsExpanded) {
1203 if (isIntersecting(mBubbleContainer, x, y)) {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001204 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)
1205 && isIntersecting(mBubbleOverflow.getBtn(), x, y)) {
Lyn Han3cd75d72020-02-15 19:10:12 -08001206 return mBubbleOverflow.getBtn();
Lyn Han99e457f2020-01-31 10:14:19 -08001207 }
Mady Mellor47b11e32019-07-11 19:06:21 -07001208 // Could be tapping or dragging a bubble while expanded
Lyn Hanc47e1712020-01-28 21:43:34 -08001209 for (int i = 0; i < getBubbleCount(); i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001210 BadgedImageView view = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001211 if (isIntersecting(view, x, y)) {
1212 return view;
1213 }
1214 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001215 }
Mady Mellor47b11e32019-07-11 19:06:21 -07001216 BubbleExpandedView bev = (BubbleExpandedView) mExpandedViewContainer.getChildAt(0);
1217 if (bev.intersectingTouchableContent((int) x, (int) y)) {
1218 return bev;
1219 }
1220 // Outside of the parts we care about.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001221 return null;
Joshua Tsuji6549e702019-05-02 13:13:16 -04001222 } else if (mFlyout.getVisibility() == VISIBLE && isIntersecting(mFlyout, x, y)) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001223 return mFlyout;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001224 } else if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1225 View bubbleChild = mBubbleContainer.getChildAt(0);
1226 if (isIntersecting(bubbleChild, x, y)) {
1227 return this;
1228 } else if (isIntersecting(mUserEducationView, x, y)) {
1229 return mUserEducationView;
1230 } else {
1231 return null;
1232 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001233 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001234
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001235 // 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 -08001236 return this;
1237 }
1238
Mark Renouf71a3af62019-04-08 15:02:54 -04001239 View getFlyoutView() {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001240 return mFlyout;
1241 }
1242
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001243 /**
Joshua Tsujif5c6a9c2020-02-25 17:47:59 -05001244 * @deprecated use {@link #setExpanded(boolean)} and
1245 * {@link BubbleData#setSelectedBubble(Bubble)}
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001246 */
1247 @Deprecated
1248 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -08001249 void collapseStack(Runnable endRunnable) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001250 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001251 Log.d(TAG, "collapseStack(endRunnable)");
1252 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001253 mBubbleData.setExpanded(false);
Mady Mellor9801e852019-01-22 14:50:28 -08001254 // TODO - use the runnable at end of animation
1255 endRunnable.run();
1256 }
1257
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001258 void showExpandedViewContents(int displayId) {
1259 if (mExpandedBubble != null
1260 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
1261 mExpandedBubble.setContentVisibility(true);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001262 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001263 }
1264
Lyn Han285ad302019-05-29 19:01:39 -07001265 private void beforeExpandedViewAnimation() {
1266 hideFlyoutImmediate();
1267 updateExpandedBubble();
1268 updateExpandedView();
1269 mIsExpansionAnimating = true;
1270 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001271
Lyn Han285ad302019-05-29 19:01:39 -07001272 private void afterExpandedViewAnimation() {
1273 updateExpandedView();
1274 mIsExpansionAnimating = false;
1275 requestUpdate();
1276 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001277
Lyn Han285ad302019-05-29 19:01:39 -07001278 private void animateCollapse() {
1279 mIsExpanded = false;
Lyn Han3cd75d72020-02-15 19:10:12 -08001280 final BubbleViewProvider previouslySelected = mExpandedBubble;
Lyn Han285ad302019-05-29 19:01:39 -07001281 beforeExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001282 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001283
Lyn Hanb58c7562020-01-07 14:29:20 -08001284 if (DEBUG_BUBBLE_STACK_VIEW) {
1285 Log.d(TAG, "animateCollapse");
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001286 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
Lyn Han9f66c3b2020-03-05 23:59:29 -08001287 mExpandedBubble));
Lyn Hanb58c7562020-01-07 14:29:20 -08001288 }
1289 updateOverflowBtnVisibility(/* apply */ false);
Lyn Han285ad302019-05-29 19:01:39 -07001290 mBubbleContainer.cancelAllAnimations();
1291 mExpandedAnimationController.collapseBackToStack(
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001292 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
1293 /* collapseTo */,
Lyn Han285ad302019-05-29 19:01:39 -07001294 () -> {
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001295 mBubbleContainer.setActiveController(mStackAnimationController);
Lyn Han285ad302019-05-29 19:01:39 -07001296 afterExpandedViewAnimation();
Lyn Han3cd75d72020-02-15 19:10:12 -08001297 previouslySelected.setContentVisibility(false);
Lyn Han285ad302019-05-29 19:01:39 -07001298 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001299
Lyn Han285ad302019-05-29 19:01:39 -07001300 mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
1301 mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
1302 mExpandedViewContainer.animate()
1303 .setDuration(100)
1304 .alpha(0f);
1305 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001306
Lyn Han285ad302019-05-29 19:01:39 -07001307 private void animateExpansion() {
1308 mIsExpanded = true;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001309 hideStackUserEducation(true /* fromExpansion */);
Lyn Han285ad302019-05-29 19:01:39 -07001310 beforeExpandedViewAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001311
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001312 mBubbleContainer.setActiveController(mExpandedAnimationController);
Lyn Hanb58c7562020-01-07 14:29:20 -08001313 updateOverflowBtnVisibility(/* apply */ false);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001314 mExpandedAnimationController.expandFromStack(() -> {
1315 updatePointerPosition();
1316 afterExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001317 maybeShowManageEducation(true);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001318 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001319
Lyn Han285ad302019-05-29 19:01:39 -07001320 mExpandedViewContainer.setTranslationX(getCollapsedX());
1321 mExpandedViewContainer.setTranslationY(getCollapsedY());
1322 mExpandedViewContainer.setAlpha(0f);
1323
1324 mExpandedViewXAnim.animateToFinalPosition(0f);
1325 mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1326 mExpandedViewContainer.animate()
1327 .setDuration(100)
1328 .alpha(1f);
1329 }
1330
1331 private float getCollapsedX() {
1332 return mStackAnimationController.getStackPosition().x < getWidth() / 2
1333 ? -mExpandedAnimateXDistance
1334 : mExpandedAnimateXDistance;
1335 }
1336
1337 private float getCollapsedY() {
1338 return Math.min(mStackAnimationController.getStackPosition().y,
1339 mExpandedAnimateYDistance);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001340 }
1341
Lyn Han3cd75d72020-02-15 19:10:12 -08001342 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
Mady Mellor99a302602019-06-14 11:39:56 -07001343 if (mExpandListener != null && bubble != null) {
1344 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001345 }
1346 }
1347
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001348 /** Return the BubbleView at the given index from the bubble container. */
Mady Mellorb8aaf972019-11-26 10:28:00 -08001349 public BadgedImageView getBubbleAt(int i) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001350 return getBubbleCount() > i
Mady Mellorb8aaf972019-11-26 10:28:00 -08001351 ? (BadgedImageView) mBubbleContainer.getChildAt(i)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001352 : null;
1353 }
1354
Joshua Tsujia19515f2019-02-13 18:02:29 -05001355 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
1356 public void onImeVisibilityChanged(boolean visible, int height) {
Mady Mellordf611cf2019-08-21 17:28:49 -07001357 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
Joshua Tsuji4b395912019-04-19 17:18:40 -04001358
Joshua Tsujiff6b0f22020-03-09 14:55:19 -04001359 if (!mIsExpanded && getBubbleCount() > 0) {
1360 final float stackDestinationY =
1361 mStackAnimationController.animateForImeVisibility(visible);
1362
1363 // How far the stack is animating due to IME, we'll just animate the flyout by that
1364 // much too.
1365 final float stackDy =
1366 stackDestinationY - mStackAnimationController.getStackPosition().y;
1367
1368 // If the flyout is visible, translate it along with the bubble stack.
1369 if (mFlyout.getVisibility() == VISIBLE) {
1370 PhysicsAnimator.getInstance(mFlyout)
1371 .spring(DynamicAnimation.TRANSLATION_Y,
1372 mFlyout.getTranslationY() + stackDy,
1373 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
1374 .start();
1375 }
Joshua Tsujia19515f2019-02-13 18:02:29 -05001376 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001377 }
1378
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001379 /** Called when the collapsed stack is tapped on. */
1380 void onStackTapped() {
1381 if (!maybeShowStackUserEducation()) {
1382 mBubbleData.setExpanded(true);
1383 }
1384 }
1385
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001386 /** Called when a drag operation on an individual bubble has started. */
Joshua Tsuji442b6272019-02-08 13:23:43 -05001387 public void onBubbleDragStart(View bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001388 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001389 Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
1390 }
Joshua Tsujibc7744b2020-03-04 15:04:59 -05001391
Joshua Tsujia2433db2020-03-12 17:56:22 -04001392 if (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView())) {
Joshua Tsujibc7744b2020-03-04 15:04:59 -05001393 return;
1394 }
1395
Joshua Tsuji20103542020-02-18 14:06:28 -05001396 mExpandedAnimationController.prepareForBubbleDrag(bubble, mMagneticTarget);
1397
1398 // We're dragging an individual bubble, so set the magnetized object to the magnetized
1399 // bubble.
1400 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
1401 mMagnetizedObject.setMagnetListener(mIndividualBubbleMagnetListener);
Joshua Tsujidb1d2e882020-03-10 15:09:08 -04001402
1403 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001404 }
1405
1406 /** Called with the coordinates to which an individual bubble has been dragged. */
Joshua Tsuji442b6272019-02-08 13:23:43 -05001407 public void onBubbleDragged(View bubble, float x, float y) {
Joshua Tsujia2433db2020-03-12 17:56:22 -04001408 if (!mIsExpanded || mIsExpansionAnimating
1409 || (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView()))) {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001410 return;
1411 }
1412
1413 mExpandedAnimationController.dragBubbleOut(bubble, x, y);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001414 springInDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001415 }
1416
1417 /** Called when a drag operation on an individual bubble has finished. */
Joshua Tsuji442b6272019-02-08 13:23:43 -05001418 public void onBubbleDragFinish(
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001419 View bubble, float x, float y, float velX, float velY) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001420 if (DEBUG_BUBBLE_STACK_VIEW) {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001421 Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001422 }
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001423
Joshua Tsujia2433db2020-03-12 17:56:22 -04001424 if (!mIsExpanded || mIsExpansionAnimating
1425 || (mBubbleOverflow != null && bubble.equals(mBubbleOverflow.getIconView()))) {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001426 return;
1427 }
1428
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001429 mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
Lyn Han634483c2019-06-28 16:52:47 -07001430 hideDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001431 }
1432
Joshua Tsuji7be4c612020-03-04 14:58:14 -05001433 /** Expands the clicked bubble. */
1434 public void expandBubble(Bubble bubble) {
1435 if (bubble.equals(mBubbleData.getSelectedBubble())) {
1436 // If the bubble we're supposed to expand is the selected bubble, that means the
1437 // overflow bubble is currently expanded. Don't tell BubbleData to set this bubble as
1438 // selected, since it already is. Just call the stack's setSelectedBubble to expand it.
1439 setSelectedBubble(bubble);
1440 } else {
1441 mBubbleData.setSelectedBubble(bubble);
1442 }
1443 }
1444
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001445 void onDragStart() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001446 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001447 Log.d(TAG, "onDragStart()");
1448 }
Mady Mellorbc078c22019-03-26 17:10:34 -07001449 if (mIsExpanded || mIsExpansionAnimating) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001450 if (DEBUG_BUBBLE_STACK_VIEW) {
1451 Log.d(TAG, "mIsExpanded or mIsExpansionAnimating");
1452 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001453 return;
1454 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001455 mStackAnimationController.cancelStackPositionAnimations();
Joshua Tsujic36ee6f2019-05-28 17:00:16 -04001456 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001457 hideFlyoutImmediate();
1458
Joshua Tsuji20103542020-02-18 14:06:28 -05001459 // Since we're dragging the stack, set the magnetized object to the magnetized stack.
1460 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
1461 mMagnetizedObject.setMagnetListener(mStackMagnetListener);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001462 }
1463
1464 void onDragged(float x, float y) {
Mady Mellorbc078c22019-03-26 17:10:34 -07001465 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001466 return;
1467 }
1468
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001469 hideStackUserEducation(false /* fromExpansion */);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001470 springInDismissTarget();
1471 mStackAnimationController.moveStackFromTouch(x, y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001472 }
1473
1474 void onDragFinish(float x, float y, float velX, float velY) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001475 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001476 Log.d(TAG, "onDragFinish");
1477 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001478
Mady Mellorbc078c22019-03-26 17:10:34 -07001479 if (mIsExpanded || mIsExpansionAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001480 return;
1481 }
1482
Joshua Tsuji6549e702019-05-02 13:13:16 -04001483 final float newStackX = mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
Steven Wua254dab2019-01-29 11:30:39 -05001484 logBubbleEvent(null /* no bubble associated with bubble stack move */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001485 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001486
Joshua Tsuji6549e702019-05-02 13:13:16 -04001487 mStackOnLeftOrWillBe = newStackX <= 0;
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001488 updateBubbleZOrdersAndDotPosition(true /* animate */);
Lyn Han634483c2019-06-28 16:52:47 -07001489 hideDismissTarget();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001490 }
1491
Joshua Tsuji6549e702019-05-02 13:13:16 -04001492 void onFlyoutDragStart() {
1493 mFlyout.removeCallbacks(mHideFlyout);
1494 }
1495
1496 void onFlyoutDragged(float deltaX) {
Joshua Tsuji8e05aab2019-08-22 14:57:50 -04001497 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1498 // is continually called.
1499 if (mFlyout.getWidth() <= 0) {
1500 return;
1501 }
1502
Joshua Tsuji6549e702019-05-02 13:13:16 -04001503 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1504 mFlyoutDragDeltaX = deltaX;
1505
1506 final float collapsePercent =
1507 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1508 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1509
Lyn Han61d5d562019-07-01 17:39:38 -07001510 // Calculate how to translate the flyout if it has been dragged too far in either direction.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001511 float overscrollTranslation = 0f;
1512 if (collapsePercent < 0f || collapsePercent > 1f) {
1513 // Whether we are more than 100% transitioned to the dot.
1514 final boolean overscrollingPastDot = collapsePercent > 1f;
1515
1516 // Whether we are overscrolling physically to the left - this can either be pulling the
1517 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1518 // after it has already become the dot.
1519 final boolean overscrollingLeft =
1520 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001521 overscrollTranslation =
1522 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1523 * (overscrollingLeft ? -1 : 1)
1524 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001525 // Attenuate the smaller dot less than the larger flyout.
1526 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001527 }
1528
1529 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1530 }
1531
Joshua Tsuji14e68552019-06-06 17:17:08 -04001532 void onFlyoutTapped() {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001533 if (maybeShowStackUserEducation()) {
1534 // If we're showing user education, don't open the bubble show the education first
1535 mBubbleToExpandAfterFlyoutCollapse = null;
1536 } else {
1537 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
1538 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001539
1540 mFlyout.removeCallbacks(mHideFlyout);
1541 mHideFlyout.run();
1542 }
1543
1544 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -04001545 * Called when the flyout drag has finished, and returns true if the gesture successfully
1546 * dismissed the flyout.
1547 */
1548 void onFlyoutDragFinished(float deltaX, float velX) {
1549 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1550 final boolean metRequiredVelocity =
1551 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
1552 final boolean metRequiredDeltaX =
1553 onLeft
1554 ? deltaX < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
1555 : deltaX > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
1556 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
1557 final boolean shouldDismiss = metRequiredVelocity || (metRequiredDeltaX && !isCancelFling);
1558
1559 mFlyout.removeCallbacks(mHideFlyout);
1560 animateFlyoutCollapsed(shouldDismiss, velX);
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001561
1562 maybeShowStackUserEducation();
Joshua Tsuji6549e702019-05-02 13:13:16 -04001563 }
1564
1565 /**
1566 * Called when the first touch event of a gesture (stack drag, bubble drag, flyout drag, etc.)
1567 * is received.
1568 */
1569 void onGestureStart() {
1570 mIsGestureInProgress = true;
1571 }
1572
1573 /** Called when a gesture is completed or cancelled. */
1574 void onGestureFinished() {
1575 mIsGestureInProgress = false;
Joshua Tsujif49ee142019-05-29 16:32:01 -04001576
1577 if (mIsExpanded) {
1578 mExpandedAnimationController.onGestureFinished();
1579 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001580 }
1581
Joshua Tsuji20103542020-02-18 14:06:28 -05001582 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
1583 boolean passEventToMagnetizedObject(MotionEvent event) {
1584 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
1585 }
1586
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001587 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1588 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1589 mDesaturateAndDarkenTargetView = targetView;
1590
1591 if (desaturateAndDarken) {
1592 // Use the animated paint for the bubbles.
1593 mDesaturateAndDarkenTargetView.setLayerType(
1594 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1595 mDesaturateAndDarkenAnimator.removeAllListeners();
1596 mDesaturateAndDarkenAnimator.start();
1597 } else {
1598 mDesaturateAndDarkenAnimator.removeAllListeners();
1599 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1600 @Override
1601 public void onAnimationEnd(Animator animation) {
1602 super.onAnimationEnd(animation);
1603 // Stop using the animated paint.
1604 resetDesaturationAndDarken();
1605 }
1606 });
1607 mDesaturateAndDarkenAnimator.reverse();
1608 }
1609 }
1610
1611 private void resetDesaturationAndDarken() {
1612 mDesaturateAndDarkenAnimator.removeAllListeners();
1613 mDesaturateAndDarkenAnimator.cancel();
1614 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1615 }
1616
Lyn Han634483c2019-06-28 16:52:47 -07001617 /** Animates in the dismiss target. */
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001618 private void springInDismissTarget() {
1619 if (mShowingDismiss) {
1620 return;
1621 }
1622
1623 mShowingDismiss = true;
1624
Joshua Tsuji20103542020-02-18 14:06:28 -05001625 mDismissTargetContainer.bringToFront();
1626 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
1627 mDismissTargetContainer.setVisibility(VISIBLE);
1628
1629 mDismissTargetAnimator.cancel();
1630 mDismissTargetAnimator
1631 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
1632 .start();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001633 }
1634
1635 /**
1636 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1637 * were dragged into the target and encircled.
1638 */
Lyn Han634483c2019-06-28 16:52:47 -07001639 private void hideDismissTarget() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001640 if (!mShowingDismiss) {
1641 return;
1642 }
1643
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001644 mShowingDismiss = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001645
Joshua Tsuji20103542020-02-18 14:06:28 -05001646 mDismissTargetAnimator
1647 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
1648 mDismissTargetSpring)
1649 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
1650 .start();
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001651 }
1652
Joshua Tsuji6549e702019-05-02 13:13:16 -04001653 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1654 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1655 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001656 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1657 // faster.
1658 mFlyoutTransitionSpring.getSpring().setStiffness(
1659 (mBubbleToExpandAfterFlyoutCollapse != null)
1660 ? SpringForce.STIFFNESS_MEDIUM
1661 : SpringForce.STIFFNESS_LOW);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001662 mFlyoutTransitionSpring
1663 .setStartValue(mFlyoutDragDeltaX)
1664 .setStartVelocity(velX)
1665 .animateToFinalPosition(collapsed
1666 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1667 : 0f);
1668 }
1669
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001670 /**
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001671 * Calculates the y position of the expanded view when it is expanded.
1672 */
Lyn Han285ad302019-05-29 19:01:39 -07001673 float getExpandedViewY() {
Lyn Han4a8efe32019-05-30 09:43:27 -07001674 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001675 }
1676
1677 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001678 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1679 */
1680 @VisibleForTesting
1681 void animateInFlyoutForBubble(Bubble bubble) {
Mady Mellordf898fd2020-01-09 09:26:36 -08001682 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001683 final BadgedImageView bubbleView = bubble.getIconView();
Mady Mellordf898fd2020-01-09 09:26:36 -08001684 if (flyoutMessage == null
1685 || flyoutMessage.message == null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001686 || !bubble.showFlyout()
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001687 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
Mark Renoufc19b4732019-06-26 12:08:33 -04001688 || isExpanded()
1689 || mIsExpansionAnimating
Joshua Tsuji14e68552019-06-06 17:17:08 -04001690 || mIsGestureInProgress
Lyn Hanf1f2c332019-08-23 17:06:56 -07001691 || mBubbleToExpandAfterFlyoutCollapse != null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001692 || bubbleView == null) {
1693 if (bubbleView != null) {
1694 bubbleView.setDotState(DOT_STATE_DEFAULT);
1695 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001696 // Skip the message if none exists, we're expanded or animating expansion, or we're
Lyn Hanf1f2c332019-08-23 17:06:56 -07001697 // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
Mark Renoufc19b4732019-06-26 12:08:33 -04001698 return;
1699 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001700
Lyn Hanf1f2c332019-08-23 17:06:56 -07001701 mFlyoutDragDeltaX = 0f;
1702 clearFlyoutOnHide();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001703 mAfterFlyoutHidden = () -> {
1704 // Null it out to ensure it runs once.
1705 mAfterFlyoutHidden = null;
1706
1707 if (mBubbleToExpandAfterFlyoutCollapse != null) {
1708 // User tapped on the flyout and we should expand
1709 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1710 mBubbleData.setExpanded(true);
1711 mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001712 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001713 bubbleView.setDotState(DOT_STATE_DEFAULT);
Lyn Hanf1f2c332019-08-23 17:06:56 -07001714 };
1715 mFlyout.setVisibility(INVISIBLE);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001716
Mady Mellorb8aaf972019-11-26 10:28:00 -08001717 // Don't show the dot when we're animating the flyout
1718 bubbleView.setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001719
Lyn Hanf1f2c332019-08-23 17:06:56 -07001720 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1721 post(() -> {
1722 // An auto-expanding bubble could have been posted during the time it takes to
1723 // layout.
1724 if (isExpanded()) {
1725 return;
1726 }
1727 final Runnable expandFlyoutAfterDelay = () -> {
1728 mAnimateInFlyout = () -> {
1729 mFlyout.setVisibility(VISIBLE);
1730 mFlyoutDragDeltaX =
1731 mStackAnimationController.isStackOnLeftSide()
1732 ? -mFlyout.getWidth()
1733 : mFlyout.getWidth();
1734 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1735 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001736 };
Lyn Hanf1f2c332019-08-23 17:06:56 -07001737 mFlyout.postDelayed(mAnimateInFlyout, 200);
1738 };
Mady Mellordf898fd2020-01-09 09:26:36 -08001739 mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
1740 mStackAnimationController.getStackPosition(), getWidth(),
Lyn Hanf1f2c332019-08-23 17:06:56 -07001741 mStackAnimationController.isStackOnLeftSide(),
Mady Mellor05e860b2019-10-30 22:48:15 -07001742 bubble.getIconView().getDotColor() /* dotColor */,
Lyn Hanf1f2c332019-08-23 17:06:56 -07001743 expandFlyoutAfterDelay /* onLayoutComplete */,
Mady Mellorb8aaf972019-11-26 10:28:00 -08001744 mAfterFlyoutHidden,
1745 bubble.getIconView().getDotCenter(),
1746 !bubble.showDot());
Lyn Hanf1f2c332019-08-23 17:06:56 -07001747 mFlyout.bringToFront();
1748 });
Mark Renoufc19b4732019-06-26 12:08:33 -04001749 mFlyout.removeCallbacks(mHideFlyout);
1750 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001751 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001752 }
1753
1754 /** Hide the flyout immediately and cancel any pending hide runnables. */
1755 private void hideFlyoutImmediate() {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001756 clearFlyoutOnHide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001757 mFlyout.removeCallbacks(mAnimateInFlyout);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001758 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001759 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001760 }
1761
Lyn Hanf1f2c332019-08-23 17:06:56 -07001762 private void clearFlyoutOnHide() {
1763 mFlyout.removeCallbacks(mAnimateInFlyout);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001764 if (mAfterFlyoutHidden == null) {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001765 return;
1766 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001767 mAfterFlyoutHidden.run();
1768 mAfterFlyoutHidden = null;
Lyn Hanf1f2c332019-08-23 17:06:56 -07001769 }
1770
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001771 @Override
1772 public void getBoundsOnScreen(Rect outRect) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001773 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1774 // When user education shows then capture all touches
1775 outRect.set(0, 0, getWidth(), getHeight());
1776 return;
1777 }
1778
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001779 if (!mIsExpanded) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001780 if (getBubbleCount() > 0) {
Mady Mellor217b2e92019-02-27 11:44:16 -08001781 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1782 }
Mady Mellore9371bc2019-07-10 18:50:59 -07001783 // Increase the touch target size of the bubble
1784 outRect.top -= mBubbleTouchPadding;
1785 outRect.left -= mBubbleTouchPadding;
1786 outRect.right += mBubbleTouchPadding;
1787 outRect.bottom += mBubbleTouchPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001788 } else {
1789 mBubbleContainer.getBoundsOnScreen(outRect);
1790 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001791
Joshua Tsuji6549e702019-05-02 13:13:16 -04001792 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001793 final Rect flyoutBounds = new Rect();
1794 mFlyout.getBoundsOnScreen(flyoutBounds);
1795 outRect.union(flyoutBounds);
1796 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001797 }
1798
1799 private int getStatusBarHeight() {
1800 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001801 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001802 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05001803 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001804 insets.getDisplayCutout() != null
1805 ? insets.getDisplayCutout().getSafeInsetTop()
1806 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001807 }
1808
1809 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001810 }
1811
1812 private boolean isIntersecting(View view, float x, float y) {
1813 mTempLoc = view.getLocationOnScreen();
1814 mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
1815 mTempLoc[1] + view.getHeight());
1816 return mTempRect.contains(x, y);
1817 }
1818
1819 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07001820 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001821 return;
1822 }
1823 mViewUpdatedRequested = true;
1824 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1825 invalidate();
1826 }
1827
1828 private void updateExpandedBubble() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001829 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001830 Log.d(TAG, "updateExpandedBubble()");
1831 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001832 mExpandedViewContainer.removeAllViews();
Lyn Han3cd75d72020-02-15 19:10:12 -08001833 if (mIsExpanded && mExpandedBubble != null) {
1834 BubbleExpandedView bev = mExpandedBubble.getExpandedView();
Lyn Hana0bb02e2020-01-28 17:57:27 -08001835 mExpandedViewContainer.addView(bev);
1836 bev.populateExpandedView();
1837 mExpandedViewContainer.setVisibility(VISIBLE);
Issei Suzukic0387542019-03-08 17:31:14 +01001838 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001839 }
1840 }
1841
Lyn Han285ad302019-05-29 19:01:39 -07001842 private void updateExpandedView() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001843 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Han285ad302019-05-29 19:01:39 -07001844 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001845 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001846
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001847 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08001848 if (mIsExpanded) {
Lyn Han285ad302019-05-29 19:01:39 -07001849 final float y = getExpandedViewY();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001850 if (!mExpandedViewYAnim.isRunning()) {
1851 // We're not animating so set the value
1852 mExpandedViewContainer.setTranslationY(y);
Lyn Han3cd75d72020-02-15 19:10:12 -08001853 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001854 mExpandedBubble.getExpandedView().updateView();
1855 }
Mady Mellor5d8f1402019-02-21 18:23:52 -08001856 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07001857 // We are animating so update the value; there is an end listener on the animator
1858 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08001859 mExpandedViewYAnim.animateToFinalPosition(y);
1860 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001861 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001862
Joshua Tsuji6549e702019-05-02 13:13:16 -04001863 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001864 updateBubbleZOrdersAndDotPosition(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001865 }
1866
1867 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001868 private void updateBubbleZOrdersAndDotPosition(boolean animate) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001869 int bubbleCount = getBubbleCount();
Lyn Han1b4f25e2019-06-11 13:56:34 -07001870 for (int i = 0; i < bubbleCount; i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001871 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellor70958542019-09-24 17:12:46 -07001872 bv.setZ((mMaxBubbles * mBubbleElevation) - i);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001873 // If the dot is on the left, and so is the stack, we need to change the dot position.
1874 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
1875 bv.setDotPosition(!mStackOnLeftOrWillBe, animate);
1876 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001877 }
1878 }
1879
Mady Mellorde2d4d22019-01-29 14:15:34 -08001880 private void updatePointerPosition() {
Lyn Han9f66c3b2020-03-05 23:59:29 -08001881 if (mExpandedBubble == null) {
Lyn Han522e9ff2019-05-17 13:26:13 -07001882 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08001883 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001884 int index = getBubbleIndex(mExpandedBubble);
Lyn Han522e9ff2019-05-17 13:26:13 -07001885 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1886 float halfBubble = mBubbleSize / 2f;
Mady Mellor9be3bed2019-08-21 17:26:26 -07001887 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
1888 // Padding might be adjusted for insets, so get it directly from the view
1889 bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
Lyn Han9f66c3b2020-03-05 23:59:29 -08001890 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08001891 }
1892
Steven Wua254dab2019-01-29 11:30:39 -05001893 /**
1894 * @return the number of bubbles in the stack view.
1895 */
Steven Wub00225b2019-02-08 14:27:42 -05001896 public int getBubbleCount() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001897 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1898 // Subtract 1 for the overflow button that is always in the bubble container.
1899 return mBubbleContainer.getChildCount() - 1;
1900 }
1901 return mBubbleContainer.getChildCount();
Steven Wua254dab2019-01-29 11:30:39 -05001902 }
1903
1904 /**
1905 * Finds the bubble index within the stack.
1906 *
Lyn Han3cd75d72020-02-15 19:10:12 -08001907 * @param provider the bubble view provider with the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05001908 * @return the index of the bubble view within the bubble stack. The range of the position
1909 * is between 0 and the bubble count minus 1.
1910 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001911 int getBubbleIndex(@Nullable BubbleViewProvider provider) {
Lyn Han9f66c3b2020-03-05 23:59:29 -08001912 if (provider == null) {
Steven Wua62cb6a2019-02-15 17:12:51 -05001913 return 0;
1914 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001915 return mBubbleContainer.indexOfChild(provider.getIconView());
Steven Wua254dab2019-01-29 11:30:39 -05001916 }
1917
1918 /**
1919 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
1920 */
Steven Wub00225b2019-02-08 14:27:42 -05001921 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001922 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05001923 .setScale(4, RoundingMode.CEILING.HALF_UP)
1924 .floatValue();
1925 }
1926
1927 /**
1928 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
1929 */
Steven Wub00225b2019-02-08 14:27:42 -05001930 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05001931 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05001932 .setScale(4, RoundingMode.CEILING.HALF_UP)
1933 .floatValue();
1934 }
1935
Joshua Tsujia19515f2019-02-13 18:02:29 -05001936 public PointF getStackPosition() {
1937 return mStackAnimationController.getStackPosition();
1938 }
1939
Steven Wua254dab2019-01-29 11:30:39 -05001940 /**
1941 * Logs the bubble UI event.
1942 *
1943 * @param bubble the bubble that is being interacted on. Null value indicates that
1944 * the user interaction is not specific to one bubble.
1945 * @param action the user interaction enum.
1946 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001947 private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
1948 if (bubble == null) {
1949 return;
Steven Wua254dab2019-01-29 11:30:39 -05001950 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001951 bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
1952 getNormalizedYPosition(), getBubbleIndex(bubble));
Steven Wua254dab2019-01-29 11:30:39 -05001953 }
Mark Renouf041d7262019-02-06 12:09:41 -05001954
1955 /**
1956 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
1957 * a back key down/up event pair is forwarded to the bubble Activity.
1958 */
1959 boolean performBackPressIfNeeded() {
Lyn Han3cd75d72020-02-15 19:10:12 -08001960 if (!isExpanded() || mExpandedBubble == null) {
Mark Renouf041d7262019-02-06 12:09:41 -05001961 return false;
1962 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001963 return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
Mark Renouf041d7262019-02-06 12:09:41 -05001964 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001965
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001966 /** Whether the educational view should appear for bubbles. **/
1967 private boolean shouldShowBubblesEducation() {
1968 return BubbleDebugConfig.forceShowUserEducation(getContext())
1969 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
1970 }
1971
1972 /** Whether the educational view should appear for the expanded view "manage" button. **/
1973 private boolean shouldShowManageEducation() {
1974 return BubbleDebugConfig.forceShowUserEducation(getContext())
1975 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
1976 }
1977
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001978 /** For debugging only */
1979 List<Bubble> getBubblesOnScreen() {
1980 List<Bubble> bubbles = new ArrayList<>();
Lyn Hanc47e1712020-01-28 21:43:34 -08001981 for (int i = 0; i < getBubbleCount(); i++) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001982 View child = mBubbleContainer.getChildAt(i);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001983 if (child instanceof BadgedImageView) {
1984 String key = ((BadgedImageView) child).getKey();
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001985 Bubble bubble = mBubbleData.getBubbleWithKey(key);
1986 bubbles.add(bubble);
1987 }
1988 }
1989 return bubbles;
1990 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001991}