blob: 1cabe221bef120fa3e3091d776c31cd135b32172 [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;
Issei Suzukia8d07312019-06-07 12:56:19 +020025import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080026import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
Issei Suzukia8d07312019-06-07 12:56:19 +020027import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
28import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
29
Joshua Tsuji4accf5982019-04-22 17:36:11 -040030import android.animation.Animator;
31import android.animation.AnimatorListenerAdapter;
32import android.animation.ValueAnimator;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040033import android.annotation.SuppressLint;
Lyn Han6c40fe72019-05-08 14:06:33 -070034import android.app.Notification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080035import android.content.Context;
Lyn Hanf4730312019-06-18 11:18:58 -070036import android.content.res.Configuration;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080037import android.content.res.Resources;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080038import android.content.res.TypedArray;
39import android.graphics.Color;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040040import android.graphics.ColorMatrix;
41import android.graphics.ColorMatrixColorFilter;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040042import android.graphics.Paint;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080043import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080044import android.graphics.PointF;
45import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080046import android.graphics.RectF;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040047import android.graphics.Region;
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;
Joshua Tsujibe60a582020-03-23 17:17:26 -040083import com.android.systemui.model.SysUiState;
84import com.android.systemui.shared.system.QuickStepContract;
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -080085import com.android.systemui.shared.system.SysUiStatsLog;
Joshua Tsujiba9fef02020-04-09 17:40:35 -040086import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
Joshua Tsuji20103542020-02-18 14:06:28 -050087import com.android.systemui.util.DismissCircleView;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050088import com.android.systemui.util.FloatingContentCoordinator;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040089import com.android.systemui.util.RelativeTouchListener;
Joshua Tsuji20103542020-02-18 14:06:28 -050090import com.android.systemui.util.animation.PhysicsAnimator;
91import com.android.systemui.util.magnetictarget.MagnetizedObject;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080092
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040093import java.io.FileDescriptor;
94import java.io.PrintWriter;
Steven Wua254dab2019-01-29 11:30:39 -050095import java.math.BigDecimal;
96import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040097import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -040098import java.util.Collections;
99import java.util.List;
Steven Wua254dab2019-01-29 11:30:39 -0500100
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800101/**
102 * Renders bubbles in a stack and handles animating expanded and collapsed states.
103 */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500104public class BubbleStackView extends FrameLayout {
Issei Suzukia8d07312019-06-07 12:56:19 +0200105 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800106
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800107 /** Animation durations for bubble stack user education views. **/
108 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
109 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
110
Joshua Tsuji6549e702019-05-02 13:13:16 -0400111 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
112 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
113
114 /** Velocity required to dismiss the flyout via drag. */
115 private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
116
117 /**
118 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
119 * for every 8 pixels overscrolled).
120 */
121 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
122
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400123 /** Duration of the flyout alpha animations. */
124 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
125
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400126 /** Percent to darken the bubbles when they're in the dismiss target. */
127 private static final float DARKEN_PERCENT = 0.3f;
128
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400129 /** How long to wait, in milliseconds, before hiding the flyout. */
130 @VisibleForTesting
131 static final int FLYOUT_HIDE_AFTER = 5000;
132
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400133 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
134 new PhysicsAnimator.SpringConfig(
135 StackAnimationController.IME_ANIMATION_STIFFNESS,
136 StackAnimationController.DEFAULT_BOUNCINESS);
137
Issei Suzukic0387542019-03-08 17:31:14 +0100138 /**
139 * Interface to synchronize {@link View} state and the screen.
140 *
141 * {@hide}
142 */
143 interface SurfaceSynchronizer {
144 /**
145 * Wait until requested change on a {@link View} is reflected on the screen.
146 *
147 * @param callback callback to run after the change is reflected on the screen.
148 */
149 void syncSurfaceAndRun(Runnable callback);
150 }
151
152 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
153 new SurfaceSynchronizer() {
154 @Override
155 public void syncSurfaceAndRun(Runnable callback) {
156 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
157 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
158 // the requested change is reflected on the screen.
159 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
160 // surfaces, rewrite this logic with them.
161 private int mFrameWait = 2;
162
163 @Override
164 public void doFrame(long frameTimeNanos) {
165 if (--mFrameWait > 0) {
166 Choreographer.getInstance().postFrameCallback(this);
167 } else {
168 callback.run();
169 }
170 }
171 });
172 }
173 };
174
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800175 private Point mDisplaySize;
176
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800177 private final SpringAnimation mExpandedViewXAnim;
178 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800179 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800180
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400181 private final Vibrator mVibrator;
182 private final ValueAnimator mDesaturateAndDarkenAnimator;
183 private final Paint mDesaturateAndDarkenPaint = new Paint();
184
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800185 private PhysicsAnimationLayout mBubbleContainer;
186 private StackAnimationController mStackAnimationController;
187 private ExpandedAnimationController mExpandedAnimationController;
188
Mady Mellor3dff9e62019-02-05 18:12:53 -0800189 private FrameLayout mExpandedViewContainer;
190
Joshua Tsuji6549e702019-05-02 13:13:16 -0400191 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400192 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400193 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700194 /**
195 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
196 * previous one animates out.
197 */
Mady Mellorb8aaf972019-11-26 10:28:00 -0800198 private Runnable mAfterFlyoutHidden;
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800199 /**
200 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
201 * once it collapses.
202 */
203 @Nullable
204 private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400205
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400206 /** Layout change listener that moves the stack to the nearest valid position on rotation. */
Lyn Hanf4730312019-06-18 11:18:58 -0700207 private OnLayoutChangeListener mOrientationChangedListener;
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400208 /** Whether the stack was on the left side of the screen prior to rotation. */
209 private boolean mWasOnLeftBeforeRotation = false;
210 /**
211 * How far down the screen the stack was before rotation, in terms of percentage of the way down
212 * the allowable region. Defaults to -1 if not set.
213 */
214 private float mVerticalPosPercentBeforeRotation = -1;
215
Mady Mellor70958542019-09-24 17:12:46 -0700216 private int mMaxBubbles;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800217 private int mBubbleSize;
Mady Mellor70958542019-09-24 17:12:46 -0700218 private int mBubbleElevation;
Lyn Han4a8efe32019-05-30 09:43:27 -0700219 private int mBubblePaddingTop;
Mady Mellore9371bc2019-07-10 18:50:59 -0700220 private int mBubbleTouchPadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700221 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800222 private int mExpandedAnimateXDistance;
223 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700224 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500225 private int mStatusBarHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500226 private int mImeOffset;
Lyn Han3cd75d72020-02-15 19:10:12 -0800227 private BubbleViewProvider mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800228 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800229
Joshua Tsuji6549e702019-05-02 13:13:16 -0400230 /** Whether the stack is currently on the left side of the screen, or animating there. */
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400231 private boolean mStackOnLeftOrWillBe = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400232
233 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
234 private boolean mIsGestureInProgress = false;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400235
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400236 /** Description of current animation controller state. */
237 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
238 pw.println("Stack view state:");
239 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
240 pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
241 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400242 mStackAnimationController.dump(fd, pw, args);
243 mExpandedAnimationController.dump(fd, pw, args);
244 }
245
Mady Mellorcd9b1302018-11-06 18:08:04 -0800246 private BubbleController.BubbleExpandListener mExpandListener;
Joshua Tsujibe60a582020-03-23 17:17:26 -0400247 private SysUiState mSysUiState;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800248
249 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700250 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400251 private boolean mShowingDismiss = false;
252
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400253 /** The view to desaturate/darken when magneted to the dismiss target. */
254 private View mDesaturateAndDarkenTargetView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800255
Mady Mellor3dff9e62019-02-05 18:12:53 -0800256 private LayoutInflater mInflater;
257
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800258 // Used for determining view / touch intersection
259 int[] mTempLoc = new int[2];
260 RectF mTempRect = new RectF();
261
Mark Renouf821e6782019-04-01 14:17:37 -0400262 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
263
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800264 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
265 new ViewTreeObserver.OnPreDrawListener() {
266 @Override
267 public boolean onPreDraw() {
268 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Lyn Han285ad302019-05-29 19:01:39 -0700269 updateExpandedView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800270 mViewUpdatedRequested = false;
271 return true;
272 }
273 };
274
Mark Renouf821e6782019-04-01 14:17:37 -0400275 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
276 this::updateSystemGestureExcludeRects;
277
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800278 private ViewClippingUtil.ClippingParameters mClippingParameters =
279 new ViewClippingUtil.ClippingParameters() {
280
Lyn Han522e9ff2019-05-17 13:26:13 -0700281 @Override
282 public boolean shouldFinish(View view) {
283 return false;
284 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800285
Lyn Han522e9ff2019-05-17 13:26:13 -0700286 @Override
287 public boolean isClippingEnablingAllowed(View view) {
288 return !mIsExpanded;
289 }
290 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800291
Joshua Tsuji6549e702019-05-02 13:13:16 -0400292 /** Float property that 'drags' the flyout. */
293 private final FloatPropertyCompat mFlyoutCollapseProperty =
294 new FloatPropertyCompat("FlyoutCollapseSpring") {
295 @Override
296 public float getValue(Object o) {
297 return mFlyoutDragDeltaX;
298 }
299
300 @Override
301 public void setValue(Object o, float v) {
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400302 setFlyoutStateForDragLength(v);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400303 }
304 };
305
306 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
307 private final SpringAnimation mFlyoutTransitionSpring =
308 new SpringAnimation(this, mFlyoutCollapseProperty);
309
310 /** Distance the flyout has been dragged in the X axis. */
311 private float mFlyoutDragDeltaX = 0f;
312
313 /**
Joshua Tsuji14e68552019-06-06 17:17:08 -0400314 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
315 */
316 private Runnable mAnimateInFlyout;
317
318 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -0400319 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
320 * it immediately.
321 */
322 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
323 (dynamicAnimation, b, v, v1) -> {
324 if (mFlyoutDragDeltaX == 0) {
325 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
326 } else {
327 mFlyout.hideFlyout();
328 }
329 };
330
Lyn Han1b4f25e2019-06-11 13:56:34 -0700331 @NonNull
332 private final SurfaceSynchronizer mSurfaceSynchronizer;
Issei Suzukic0387542019-03-08 17:31:14 +0100333
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400334 private final NotificationShadeWindowController mNotificationShadeWindowController;
335
Joshua Tsuji20103542020-02-18 14:06:28 -0500336 /**
337 * The currently magnetized object, which is being dragged and will be attracted to the magnetic
338 * dismiss target.
339 *
340 * This is either the stack itself, or an individual bubble.
341 */
342 private MagnetizedObject<?> mMagnetizedObject;
343
344 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500345 * The MagneticTarget instance for our circular dismiss view. This is added to the
346 * MagnetizedObject instances for the stack and any dragged-out bubbles.
347 */
348 private MagnetizedObject.MagneticTarget mMagneticTarget;
349
350 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
351 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
352 new MagnetizedObject.MagnetListener() {
353 @Override
354 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
355 animateDesaturateAndDarken(
356 mExpandedAnimationController.getDraggedOutBubble(), true);
357 }
358
359 @Override
360 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
361 float velX, float velY, boolean wasFlungOut) {
362 animateDesaturateAndDarken(
363 mExpandedAnimationController.getDraggedOutBubble(), false);
364
365 if (wasFlungOut) {
366 mExpandedAnimationController.snapBubbleBack(
367 mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
368 hideDismissTarget();
369 } else {
370 mExpandedAnimationController.onUnstuckFromTarget();
371 }
372 }
373
374 @Override
375 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
376 mExpandedAnimationController.dismissDraggedOutBubble(
377 mExpandedAnimationController.getDraggedOutBubble(),
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400378 BubbleStackView.this::dismissMagnetizedObject);
Joshua Tsuji20103542020-02-18 14:06:28 -0500379 hideDismissTarget();
380 }
381 };
382
383 /** Magnet listener that handles animating and dismissing the entire stack. */
384 private final MagnetizedObject.MagnetListener mStackMagnetListener =
385 new MagnetizedObject.MagnetListener() {
386 @Override
387 public void onStuckToTarget(
388 @NonNull MagnetizedObject.MagneticTarget target) {
389 animateDesaturateAndDarken(mBubbleContainer, true);
390 }
391
392 @Override
393 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
394 float velX, float velY, boolean wasFlungOut) {
395 animateDesaturateAndDarken(mBubbleContainer, false);
396
397 if (wasFlungOut) {
398 mStackAnimationController.flingStackThenSpringToEdge(
399 mStackAnimationController.getStackPosition().x, velX, velY);
400 hideDismissTarget();
401 } else {
402 mStackAnimationController.onUnstuckFromTarget();
403 }
404 }
405
406 @Override
407 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
408 mStackAnimationController.implodeStack(
409 () -> {
410 resetDesaturationAndDarken();
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400411 dismissMagnetizedObject();
Joshua Tsuji20103542020-02-18 14:06:28 -0500412 }
413 );
414
415 hideDismissTarget();
416 }
417 };
418
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400419 /**
420 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack.
421 * When expanded, clicking a bubble either expands that bubble, or collapses the stack.
422 */
423 private OnClickListener mBubbleClickListener = new OnClickListener() {
424 @Override
425 public void onClick(View view) {
426 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view);
427
428 // If the bubble has since left us, ignore the click.
429 if (clickedBubble == null) {
430 return;
431 }
432
433 final boolean clickedBubbleIsCurrentlyExpandedBubble =
434 clickedBubble.getKey().equals(mExpandedBubble.getKey());
435
436 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) {
437 if (clickedBubble != mBubbleData.getSelectedBubble()) {
438 // Select the clicked bubble.
439 mBubbleData.setSelectedBubble(clickedBubble);
440 } else {
441 // If the clicked bubble is the selected bubble (but not the expanded bubble),
442 // that means overflow was previously expanded. Set the selected bubble
443 // internally without going through BubbleData (which would ignore it since it's
444 // already selected).
Lyn Han89fb39d2020-04-07 11:51:07 -0700445 mBubbleData.setShowingOverflow(true);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400446 setSelectedBubble(clickedBubble);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400447 }
448 } else {
449 // Otherwise, we either tapped the stack (which means we're collapsed
450 // and should expand) or the currently selected bubble (we're expanded
451 // and should collapse).
452 if (!maybeShowStackUserEducation()) {
453 mBubbleData.setExpanded(!mBubbleData.isExpanded());
454 }
455 }
456 }
457 };
458
459 /**
460 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when
461 * collapsed), or individual bubbles (when expanded).
462 */
463 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() {
464
465 @Override
466 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
467 // If we're expanding or collapsing, consume but ignore all touch events.
468 if (mIsExpansionAnimating) {
469 return true;
470 }
471
472 if (mBubbleData.isExpanded()) {
473 maybeShowManageEducation(false /* show */);
474
475 // If we're expanded, tell the animation controller to prepare to drag this bubble,
476 // dispatching to the individual bubble magnet listener.
477 mExpandedAnimationController.prepareForBubbleDrag(
478 v /* bubble */,
479 mMagneticTarget,
480 mIndividualBubbleMagnetListener);
481
482 // Save the magnetized individual bubble so we can dispatch touch events to it.
483 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
484 } else {
485 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the
486 // animation controller, and hide the flyout.
487 mStackAnimationController.cancelStackPositionAnimations();
488 mBubbleContainer.setActiveController(mStackAnimationController);
489 hideFlyoutImmediate();
490
491 // Also, save the magnetized stack so we can dispatch touch events to it.
492 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
493 mMagnetizedObject.setMagnetListener(mStackMagnetListener);
494 }
495
496 passEventToMagnetizedObject(ev);
497
498 // Bubbles are always interested in all touch events!
499 return true;
500 }
501
502 @Override
503 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
504 float viewInitialY, float dx, float dy) {
505 // If we're expanding or collapsing, ignore all touch events.
506 if (mIsExpansionAnimating) {
507 return;
508 }
509
510 // Show the dismiss target, if we haven't already.
511 springInDismissTargetMaybe();
512
513 // First, see if the magnetized object consumes the event - if so, we shouldn't move the
514 // bubble since it's stuck to the target.
515 if (!passEventToMagnetizedObject(ev)) {
516 if (mBubbleData.isExpanded()) {
517 mExpandedAnimationController.dragBubbleOut(
518 v, viewInitialX + dx, viewInitialY + dy);
519 } else {
520 hideStackUserEducation(false /* fromExpansion */);
521 mStackAnimationController.moveStackFromTouch(
522 viewInitialX + dx, viewInitialY + dy);
523 }
524 }
525 }
526
527 @Override
528 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
529 float viewInitialY, float dx, float dy, float velX, float velY) {
530 // If we're expanding or collapsing, ignore all touch events.
531 if (mIsExpansionAnimating) {
532 return;
533 }
534
535 // First, see if the magnetized object consumes the event - if so, the bubble was
536 // released in the target or flung out of it, and we should ignore the event.
537 if (!passEventToMagnetizedObject(ev)) {
538 if (mBubbleData.isExpanded()) {
539 mExpandedAnimationController.snapBubbleBack(v, velX, velY);
540 } else {
541 // Fling the stack to the edge, and save whether or not it's going to end up on
542 // the left side of the screen.
543 mStackOnLeftOrWillBe =
544 mStackAnimationController.flingStackThenSpringToEdge(
545 viewInitialX + dx, velX, velY) <= 0;
546
547 updateBubbleZOrdersAndDotPosition(true /* animate */);
548
549 logBubbleEvent(null /* no bubble associated with bubble stack move */,
550 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
551 }
552
553 hideDismissTarget();
554 }
555 }
556 };
557
558 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
559 private OnClickListener mFlyoutClickListener = new OnClickListener() {
560 @Override
561 public void onClick(View view) {
562 if (maybeShowStackUserEducation()) {
563 // If we're showing user education, don't open the bubble show the education first
564 mBubbleToExpandAfterFlyoutCollapse = null;
565 } else {
566 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
567 }
568
569 mFlyout.removeCallbacks(mHideFlyout);
570 mHideFlyout.run();
571 }
572 };
573
574 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */
575 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() {
576
577 @Override
578 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
579 mFlyout.removeCallbacks(mHideFlyout);
580 return true;
581 }
582
583 @Override
584 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
585 float viewInitialY, float dx, float dy) {
586 setFlyoutStateForDragLength(dx);
587 }
588
589 @Override
590 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
591 float viewInitialY, float dx, float dy, float velX, float velY) {
592 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
593 final boolean metRequiredVelocity =
594 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
595 final boolean metRequiredDeltaX =
596 onLeft
597 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
598 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
599 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
600 final boolean shouldDismiss = metRequiredVelocity
601 || (metRequiredDeltaX && !isCancelFling);
602
603 mFlyout.removeCallbacks(mHideFlyout);
604 animateFlyoutCollapsed(shouldDismiss, velX);
605
606 maybeShowStackUserEducation();
607 }
608 };
609
Joshua Tsuji20103542020-02-18 14:06:28 -0500610 private ViewGroup mDismissTargetContainer;
611 private PhysicsAnimator<View> mDismissTargetAnimator;
612 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
613 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Issei Suzukic0387542019-03-08 17:31:14 +0100614
Lyn Hanf4730312019-06-18 11:18:58 -0700615 private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
Lyn Han3cd75d72020-02-15 19:10:12 -0800616
Joshua Tsujia2433db2020-03-12 17:56:22 -0400617 @Nullable
Lyn Han3cd75d72020-02-15 19:10:12 -0800618 private BubbleOverflow mBubbleOverflow;
Lyn Hanf4730312019-06-18 11:18:58 -0700619
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800620 private boolean mShouldShowUserEducation;
621 private boolean mAnimatingEducationAway;
622 private View mUserEducationView;
623
624 private boolean mShouldShowManageEducation;
625 private BubbleManageEducationView mManageEducationView;
626 private boolean mAnimatingManageEducationAway;
627
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400628 @SuppressLint("ClickableViewAccessibility")
Issei Suzukic0387542019-03-08 17:31:14 +0100629 public BubbleStackView(Context context, BubbleData data,
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500630 @Nullable SurfaceSynchronizer synchronizer,
Joshua Tsujibe60a582020-03-23 17:17:26 -0400631 FloatingContentCoordinator floatingContentCoordinator,
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400632 SysUiState sysUiState,
633 NotificationShadeWindowController notificationShadeWindowController) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800634 super(context);
635
Mady Mellorcfd06c12019-02-13 14:32:12 -0800636 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800637 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800638
Joshua Tsujibe60a582020-03-23 17:17:26 -0400639 mSysUiState = sysUiState;
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400640 mNotificationShadeWindowController = notificationShadeWindowController;
Joshua Tsujibe60a582020-03-23 17:17:26 -0400641
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800642 Resources res = getResources();
Mady Mellor70958542019-09-24 17:12:46 -0700643 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800644 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellor70958542019-09-24 17:12:46 -0700645 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Lyn Han4a8efe32019-05-30 09:43:27 -0700646 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellore9371bc2019-07-10 18:50:59 -0700647 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800648 mExpandedAnimateXDistance =
649 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
650 mExpandedAnimateYDistance =
651 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700652 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
653
Joshua Tsujif44347f2019-02-12 14:28:06 -0500654 mStatusBarHeight =
655 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500656 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800657
658 mDisplaySize = new Point();
659 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700660 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700661 wm.getDefaultDisplay().getRealSize(mDisplaySize);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800662
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400663 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
664
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700665 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800666 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800667
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400668 mStackAnimationController = new StackAnimationController(
669 floatingContentCoordinator, this::getBubbleCount);
Lyn Hanf4730312019-06-18 11:18:58 -0700670
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700671 mExpandedAnimationController = new ExpandedAnimationController(
Lyn Hanf4730312019-06-18 11:18:58 -0700672 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
Issei Suzukic0387542019-03-08 17:31:14 +0100673 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800674
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800675 setUpUserEducation();
676
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800677 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400678 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800679 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800680 mBubbleContainer.setClipChildren(false);
681 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
682
Mady Mellor3dff9e62019-02-05 18:12:53 -0800683 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800684 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700685 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
686 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800687 mExpandedViewContainer.setClipChildren(false);
688 addView(mExpandedViewContainer);
689
Mady Mellor8bfe5412019-07-31 14:56:44 -0700690 setUpFlyout();
Joshua Tsuji6549e702019-05-02 13:13:16 -0400691 mFlyoutTransitionSpring.setSpring(new SpringForce()
Joshua Tsuji14e68552019-06-06 17:17:08 -0400692 .setStiffness(SpringForce.STIFFNESS_LOW)
Joshua Tsuji6549e702019-05-02 13:13:16 -0400693 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
694 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
695
Joshua Tsujie48c4112020-02-26 14:36:25 -0500696 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
Joshua Tsuji20103542020-02-18 14:06:28 -0500697 final View targetView = new DismissCircleView(context);
698 final FrameLayout.LayoutParams newParams =
Joshua Tsujie48c4112020-02-26 14:36:25 -0500699 new FrameLayout.LayoutParams(targetSize, targetSize);
Joshua Tsuji20103542020-02-18 14:06:28 -0500700 newParams.gravity = Gravity.CENTER;
701 targetView.setLayoutParams(newParams);
702 mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
703
704 mDismissTargetContainer = new FrameLayout(context);
705 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
Joshua Tsuji6549e702019-05-02 13:13:16 -0400706 MATCH_PARENT,
Joshua Tsujif39539d2020-04-03 18:53:06 -0400707 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
Joshua Tsuji6549e702019-05-02 13:13:16 -0400708 Gravity.BOTTOM));
Joshua Tsuji20103542020-02-18 14:06:28 -0500709 mDismissTargetContainer.setClipChildren(false);
710 mDismissTargetContainer.addView(targetView);
711 mDismissTargetContainer.setVisibility(View.INVISIBLE);
712 addView(mDismissTargetContainer);
713
714 // Start translated down so the target springs up.
715 targetView.setTranslationY(
Joshua Tsujif39539d2020-04-03 18:53:06 -0400716 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height));
Joshua Tsuji20103542020-02-18 14:06:28 -0500717
718 // Save the MagneticTarget instance for the newly set up view - we'll add this to the
719 // MagnetizedObjects.
720 mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400721
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800722 mExpandedViewXAnim =
723 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
724 mExpandedViewXAnim.setSpring(
725 new SpringForce()
726 .setStiffness(SpringForce.STIFFNESS_LOW)
727 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
728
729 mExpandedViewYAnim =
730 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
731 mExpandedViewYAnim.setSpring(
732 new SpringForce()
733 .setStiffness(SpringForce.STIFFNESS_LOW)
734 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorbc078c22019-03-26 17:10:34 -0700735 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700736 if (mIsExpanded && mExpandedBubble != null
737 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -0800738 mExpandedBubble.getExpandedView().updateView();
Mady Mellorbc078c22019-03-26 17:10:34 -0700739 }
740 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800741
742 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800743 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500744 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800745
Lyn Hancd4f87e2020-02-19 20:33:45 -0800746 setUpOverflow();
Lyn Hanb58c7562020-01-07 14:29:20 -0800747
Mady Mellor5d8f1402019-02-21 18:23:52 -0800748 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
Mady Mellorbc078c22019-03-26 17:10:34 -0700749 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800750 return view.onApplyWindowInsets(insets);
751 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800752 mExpandedAnimationController.updateYPosition(
753 // Update the insets after we're done translating otherwise position
754 // calculation for them won't be correct.
Lyn Hanb58c7562020-01-07 14:29:20 -0800755 () -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700756 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800757 mExpandedBubble.getExpandedView().updateInsets(insets);
758 }
759 });
Mady Mellor5d8f1402019-02-21 18:23:52 -0800760 return view.onApplyWindowInsets(insets);
761 });
Mark Renouf821e6782019-04-01 14:17:37 -0400762
Lyn Hanf4730312019-06-18 11:18:58 -0700763 mOrientationChangedListener =
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400764 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
Mady Mellor9be3bed2019-08-21 17:26:26 -0700765 mExpandedAnimationController.updateOrientation(mOrientation, mDisplaySize);
766 mStackAnimationController.updateOrientation(mOrientation);
767
768 // Reposition & adjust the height for new orientation
769 if (mIsExpanded) {
770 mExpandedViewContainer.setTranslationY(getExpandedViewY());
Mady Melloradd5c6a92020-03-31 17:22:48 -0700771 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800772 mExpandedBubble.getExpandedView().updateView();
773 }
Mady Mellor9be3bed2019-08-21 17:26:26 -0700774 }
775
776 // Need to update the padding around the view
777 WindowInsets insets = getRootWindowInsets();
778 int leftPadding = mExpandedViewPadding;
779 int rightPadding = mExpandedViewPadding;
780 if (insets != null) {
781 // Can't have the expanded view overlaying notches
782 int cutoutLeft = 0;
783 int cutoutRight = 0;
784 DisplayCutout cutout = insets.getDisplayCutout();
785 if (cutout != null) {
786 cutoutLeft = cutout.getSafeInsetLeft();
787 cutoutRight = cutout.getSafeInsetRight();
788 }
789 // Or overlaying nav or status bar
790 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
791 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
792 }
793 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
794 rightPadding, mExpandedViewPadding);
795
Lyn Hanf4730312019-06-18 11:18:58 -0700796 if (mIsExpanded) {
797 // Re-draw bubble row and pointer for new orientation.
798 mExpandedAnimationController.expandFromStack(() -> {
799 updatePointerPosition();
800 } /* after */);
801 }
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400802 if (mVerticalPosPercentBeforeRotation >= 0) {
803 mStackAnimationController.moveStackToSimilarPositionAfterRotation(
804 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
805 }
Lyn Hanf4730312019-06-18 11:18:58 -0700806 removeOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400807 };
808
Mark Renouf821e6782019-04-01 14:17:37 -0400809 // This must be a separate OnDrawListener since it should be called for every draw.
810 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400811
812 final ColorMatrix animatedMatrix = new ColorMatrix();
813 final ColorMatrix darkenMatrix = new ColorMatrix();
814
815 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
816 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
817 final float animatedValue = (float) animation.getAnimatedValue();
818 animatedMatrix.setSaturation(animatedValue);
819
820 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
821 darkenMatrix.setScale(
822 1f - animatedDarkenValue /* red */,
823 1f - animatedDarkenValue /* green */,
824 1f - animatedDarkenValue /* blue */,
825 1f /* alpha */);
826
827 // Concat the matrices so that the animatedMatrix both desaturates and darkens.
828 animatedMatrix.postConcat(darkenMatrix);
829
830 // Update the paint and apply it to the bubble container.
831 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
832 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
833 });
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400834
835 // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts,
836 // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
837 setOnTouchListener((view, ev) -> {
838 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
839 if (mBubbleData.isExpanded()) {
840 mBubbleData.setExpanded(false);
841 }
842 }
843
844 return false;
845 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800846 }
847
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800848 private void setUpUserEducation() {
849 if (mUserEducationView != null) {
850 removeView(mUserEducationView);
851 }
852 mShouldShowUserEducation = shouldShowBubblesEducation();
853 if (DEBUG_USER_EDUCATION) {
854 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
855 }
856 if (mShouldShowUserEducation) {
857 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
858 false /* attachToRoot */);
859 mUserEducationView.setVisibility(GONE);
860
861 final TypedArray ta = mContext.obtainStyledAttributes(
862 new int[] {android.R.attr.colorAccent,
863 android.R.attr.textColorPrimaryInverse});
864 final int bgColor = ta.getColor(0, Color.BLACK);
865 int textColor = ta.getColor(1, Color.WHITE);
866 ta.recycle();
867 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
868
869 TextView title = mUserEducationView.findViewById(R.id.user_education_title);
870 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
871 title.setTextColor(textColor);
872 description.setTextColor(textColor);
873
874 addView(mUserEducationView);
875 }
876
877 if (mManageEducationView != null) {
878 removeView(mManageEducationView);
879 }
880 mShouldShowManageEducation = shouldShowManageEducation();
881 if (DEBUG_USER_EDUCATION) {
882 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
883 }
884 if (mShouldShowManageEducation) {
885 mManageEducationView = (BubbleManageEducationView)
886 mInflater.inflate(R.layout.bubbles_manage_button_education, this,
887 false /* attachToRoot */);
888 mManageEducationView.setVisibility(GONE);
889 mManageEducationView.setElevation(mBubbleElevation);
890
891 addView(mManageEducationView);
Lyn Hanb58c7562020-01-07 14:29:20 -0800892 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800893 }
894
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400895 @SuppressLint("ClickableViewAccessibility")
Mady Mellor8bfe5412019-07-31 14:56:44 -0700896 private void setUpFlyout() {
897 if (mFlyout != null) {
898 removeView(mFlyout);
899 }
900 mFlyout = new BubbleFlyoutView(getContext());
901 mFlyout.setVisibility(GONE);
902 mFlyout.animate()
903 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
904 .setInterpolator(new AccelerateDecelerateInterpolator());
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400905 mFlyout.setOnClickListener(mFlyoutClickListener);
906 mFlyout.setOnTouchListener(mFlyoutTouchListener);
Mady Mellor8bfe5412019-07-31 14:56:44 -0700907 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
908 }
909
Lyn Hancd4f87e2020-02-19 20:33:45 -0800910 private void setUpOverflow() {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800911 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
912 return;
913 }
Lyn Hancd4f87e2020-02-19 20:33:45 -0800914 int overflowBtnIndex = 0;
915 if (mBubbleOverflow == null) {
Mady Mellor0122cc92020-02-27 12:15:39 -0800916 mBubbleOverflow = new BubbleOverflow(getContext());
917 mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800918 } else {
919 mBubbleContainer.removeView(mBubbleOverflow.getBtn());
920 mBubbleOverflow.updateIcon(mContext, this);
Lyn Hane1cf3b22020-04-01 13:39:43 -0700921 overflowBtnIndex = mBubbleContainer.getChildCount();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800922 }
923 mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
924 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
925
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400926 mBubbleOverflow.getBtn().setOnClickListener(v -> setSelectedBubble(mBubbleOverflow));
Lyn Hancd4f87e2020-02-19 20:33:45 -0800927 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700928 /**
Lyn Han02cca812019-04-02 16:27:32 -0700929 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700930 */
Lyn Han02cca812019-04-02 16:27:32 -0700931 public void onThemeChanged() {
Mady Mellor8bfe5412019-07-31 14:56:44 -0700932 setUpFlyout();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800933 setUpOverflow();
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800934 setUpUserEducation();
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700935 }
936
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400937 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
Lyn Hanf4730312019-06-18 11:18:58 -0700938 public void onOrientationChanged(int orientation) {
939 mOrientation = orientation;
940
Mady Mellore19353d2019-08-21 17:25:02 -0700941 // Display size is based on the rotation device was in when requested, we should update it
Mady Mellor9be3bed2019-08-21 17:26:26 -0700942 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700943 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
944 wm.getDefaultDisplay().getRealSize(mDisplaySize);
945
Mady Mellor818eef02019-08-16 16:12:29 -0700946 // Some resources change depending on orientation
947 Resources res = getContext().getResources();
948 mStatusBarHeight = res.getDimensionPixelSize(
949 com.android.internal.R.dimen.status_bar_height);
950 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
951
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400952 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
953 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
954 mVerticalPosPercentBeforeRotation =
955 (mStackAnimationController.getStackPosition().y - allowablePos.top)
956 / (allowablePos.bottom - allowablePos.top);
Lyn Hanf4730312019-06-18 11:18:58 -0700957 addOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400958 hideFlyoutImmediate();
959 }
960
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800961 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800962 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
963 getBoundsOnScreen(outRect);
964 }
965
966 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800967 protected void onDetachedFromWindow() {
968 super.onDetachedFromWindow();
969 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
970 }
971
972 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800973 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
974 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Hane68d0912019-05-02 18:28:01 -0700975
976 // Custom actions.
977 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
978 getContext().getResources()
979 .getString(R.string.bubble_accessibility_action_move_top_left));
980 info.addAction(moveTopLeft);
981
982 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
983 getContext().getResources()
984 .getString(R.string.bubble_accessibility_action_move_top_right));
985 info.addAction(moveTopRight);
986
987 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
988 getContext().getResources()
989 .getString(R.string.bubble_accessibility_action_move_bottom_left));
990 info.addAction(moveBottomLeft);
991
992 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
993 getContext().getResources()
994 .getString(R.string.bubble_accessibility_action_move_bottom_right));
995 info.addAction(moveBottomRight);
996
997 // Default actions.
998 info.addAction(AccessibilityAction.ACTION_DISMISS);
Mady Mellor217b2e92019-02-27 11:44:16 -0800999 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -07001000 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -08001001 } else {
Lyn Hane68d0912019-05-02 18:28:01 -07001002 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -08001003 }
1004 }
1005
1006 @Override
1007 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1008 if (super.performAccessibilityActionInternal(action, arguments)) {
1009 return true;
1010 }
Lyn Hane68d0912019-05-02 18:28:01 -07001011 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
1012
1013 // R constants are not final so we cannot use switch-case here.
1014 if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
1015 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
1016 return true;
1017 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
1018 mBubbleData.setExpanded(false);
1019 return true;
1020 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
1021 mBubbleData.setExpanded(true);
1022 return true;
1023 } else if (action == R.id.action_move_top_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001024 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001025 return true;
1026 } else if (action == R.id.action_move_top_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001027 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001028 return true;
1029 } else if (action == R.id.action_move_bottom_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001030 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001031 return true;
1032 } else if (action == R.id.action_move_bottom_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001033 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001034 return true;
Mady Mellor217b2e92019-02-27 11:44:16 -08001035 }
1036 return false;
1037 }
1038
Lyn Han6c40fe72019-05-08 14:06:33 -07001039 /**
1040 * Update content description for a11y TalkBack.
1041 */
1042 public void updateContentDescription() {
1043 if (mBubbleData.getBubbles().isEmpty()) {
1044 return;
1045 }
1046 Bubble topBubble = mBubbleData.getBubbles().get(0);
1047 String appName = topBubble.getAppName();
Ned Burns00b4b2d2019-10-17 22:09:27 -04001048 Notification notification = topBubble.getEntry().getSbn().getNotification();
Lyn Han6c40fe72019-05-08 14:06:33 -07001049 CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
Lyn Han93cd3132020-02-18 18:22:05 -08001050 String titleStr = getResources().getString(R.string.notification_bubble_title);
Lyn Han6c40fe72019-05-08 14:06:33 -07001051 if (titleCharSeq != null) {
1052 titleStr = titleCharSeq.toString();
1053 }
1054 int moreCount = mBubbleContainer.getChildCount() - 1;
1055
1056 // Example: Title from app name.
1057 String singleDescription = getResources().getString(
1058 R.string.bubble_content_description_single, titleStr, appName);
1059
1060 // Example: Title from app name and 4 more.
1061 String stackDescription = getResources().getString(
1062 R.string.bubble_content_description_stack, titleStr, appName, moreCount);
1063
1064 if (mIsExpanded) {
1065 // TODO(b/129522932) - update content description for each bubble in expanded view.
1066 } else {
1067 // Collapsed stack.
1068 if (moreCount > 0) {
1069 mBubbleContainer.setContentDescription(stackDescription);
1070 } else {
1071 mBubbleContainer.setContentDescription(singleDescription);
1072 }
1073 }
1074 }
1075
Mark Renouf821e6782019-04-01 14:17:37 -04001076 private void updateSystemGestureExcludeRects() {
1077 // Exclude the region occupied by the first BubbleView in the stack
1078 Rect excludeZone = mSystemGestureExclusionRects.get(0);
Lyn Hanc47e1712020-01-28 21:43:34 -08001079 if (getBubbleCount() > 0) {
Mark Renouf821e6782019-04-01 14:17:37 -04001080 View firstBubble = mBubbleContainer.getChildAt(0);
1081 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
1082 firstBubble.getBottom());
1083 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
1084 (int) (firstBubble.getTranslationY() + 0.5f));
1085 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
1086 } else {
1087 excludeZone.setEmpty();
1088 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
1089 }
1090 }
1091
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001092 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -08001093 * Sets the listener to notify when the bubble stack is expanded.
1094 */
1095 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
1096 mExpandListener = listener;
1097 }
1098
1099 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001100 * Whether the stack of bubbles is expanded or not.
1101 */
1102 public boolean isExpanded() {
1103 return mIsExpanded;
1104 }
1105
1106 /**
Mady Mellor047e24e2019-08-05 11:35:40 -07001107 * Whether the stack of bubbles is animating to or from expansion.
1108 */
1109 public boolean isExpansionAnimating() {
1110 return mIsExpansionAnimating;
1111 }
1112
1113 /**
Mady Mellorb8aaf972019-11-26 10:28:00 -08001114 * The {@link BadgedImageView} that is expanded, null if one does not exist.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001115 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001116 View getExpandedBubbleView() {
Mady Mellored99c272019-06-13 15:58:30 -07001117 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
Mady Mellor3dff9e62019-02-05 18:12:53 -08001118 }
1119
1120 /**
1121 * The {@link Bubble} that is expanded, null if one does not exist.
1122 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001123 @Nullable
Lyn Han9f66c3b2020-03-05 23:59:29 -08001124 BubbleViewProvider getExpandedBubble() {
1125 return mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001126 }
1127
Mark Renouf71a3af62019-04-08 15:02:54 -04001128 // via BubbleData.Listener
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001129 @SuppressLint("ClickableViewAccessibility")
Mark Renouf71a3af62019-04-08 15:02:54 -04001130 void addBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001131 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001132 Log.d(TAG, "addBubble: " + bubble);
1133 }
Joshua Tsujib35f5912019-07-24 16:15:21 -04001134
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001135 if (getBubbleCount() == 0 && mShouldShowUserEducation) {
1136 // Override the default stack position if we're showing user education.
1137 mStackAnimationController.setStackPosition(
1138 mStackAnimationController.getDefaultStartPosition());
1139 }
1140
Lyn Hanc47e1712020-01-28 21:43:34 -08001141 if (getBubbleCount() == 0) {
Joshua Tsujib35f5912019-07-24 16:15:21 -04001142 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
1143 }
1144
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001145 if (bubble.getIconView() == null) {
1146 return;
1147 }
1148
Joshua Tsujib35f5912019-07-24 16:15:21 -04001149 // Set the dot position to the opposite of the side the stack is resting on, since the stack
1150 // resting slightly off-screen would result in the dot also being off-screen.
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001151 bubble.getIconView().setDotPositionOnLeft(
Joshua Tsujib35f5912019-07-24 16:15:21 -04001152 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
1153
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001154 bubble.getIconView().setOnClickListener(mBubbleClickListener);
1155 bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
1156
Mady Mellored99c272019-06-13 15:58:30 -07001157 mBubbleContainer.addView(bubble.getIconView(), 0,
Mark Renouf71a3af62019-04-08 15:02:54 -04001158 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellored99c272019-06-13 15:58:30 -07001159 ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
Mark Renoufba5ab512019-05-02 15:21:01 -04001160 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001161 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001162 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001163 }
1164
1165 // via BubbleData.Listener
1166 void removeBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001167 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001168 Log.d(TAG, "removeBubble: " + bubble);
1169 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001170 // Remove it from the views
Lyn Hane1395572020-03-23 15:48:54 -07001171 for (int i = 0; i < getBubbleCount(); i++) {
1172 View v = mBubbleContainer.getChildAt(i);
1173 if (v instanceof BadgedImageView
1174 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) {
1175 mBubbleContainer.removeViewAt(i);
Mady Mellore967e962020-03-26 17:36:44 -07001176 bubble.cleanupViews();
Lyn Hane1395572020-03-23 15:48:54 -07001177 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
1178 return;
1179 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001180 }
Lyn Hane1395572020-03-23 15:48:54 -07001181 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
Lyn Hanb58c7562020-01-07 14:29:20 -08001182 }
1183
1184 private void updateOverflowBtnVisibility(boolean apply) {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001185 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1186 return;
1187 }
Lyn Hanb58c7562020-01-07 14:29:20 -08001188 if (mIsExpanded) {
1189 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001190 Log.d(TAG, "Show overflow button.");
Lyn Hanb58c7562020-01-07 14:29:20 -08001191 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001192 mBubbleOverflow.setBtnVisible(VISIBLE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001193 if (apply) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001194 mExpandedAnimationController.expandFromStack(() -> {
1195 updatePointerPosition();
1196 } /* after */);
Lyn Hanb58c7562020-01-07 14:29:20 -08001197 }
1198 } else {
1199 if (DEBUG_BUBBLE_STACK_VIEW) {
1200 Log.d(TAG, "Collapsed. Hide overflow button.");
1201 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001202 mBubbleOverflow.setBtnVisible(GONE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001203 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001204 }
1205
1206 // via BubbleData.Listener
1207 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -04001208 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001209 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001210 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001211 }
1212
Mark Renoufba5ab512019-05-02 15:21:01 -04001213 public void updateBubbleOrder(List<Bubble> bubbles) {
1214 for (int i = 0; i < bubbles.size(); i++) {
1215 Bubble bubble = bubbles.get(i);
Mady Mellored99c272019-06-13 15:58:30 -07001216 mBubbleContainer.reorderView(bubble.getIconView(), i);
Mark Renoufba5ab512019-05-02 15:21:01 -04001217 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001218 updateBubbleZOrdersAndDotPosition(false /* animate */);
Lyn Hanf44562b2020-03-30 16:40:46 -07001219 updatePointerPosition();
Mark Renoufba5ab512019-05-02 15:21:01 -04001220 }
1221
Mady Melloredd4ee12019-01-18 10:45:11 -08001222 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001223 * Changes the currently selected bubble. If the stack is already expanded, the newly selected
1224 * bubble will be shown immediately. This does not change the expanded state or change the
1225 * position of any bubble.
1226 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001227 // via BubbleData.Listener
Lyn Han3cd75d72020-02-15 19:10:12 -08001228 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001229 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001230 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
1231 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001232 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
1233 return;
1234 }
Lyn Han89fb39d2020-04-07 11:51:07 -07001235 if (bubbleToSelect == null || bubbleToSelect.getKey() != BubbleOverflow.KEY) {
1236 mBubbleData.setShowingOverflow(false);
1237 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001238 final BubbleViewProvider previouslySelected = mExpandedBubble;
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001239 mExpandedBubble = bubbleToSelect;
Lyn Han89fb39d2020-04-07 11:51:07 -07001240 updatePointerPosition();
Issei Suzukicac2a502019-04-16 16:52:50 +02001241
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001242 if (mIsExpanded) {
1243 // Make the container of the expanded view transparent before removing the expanded view
1244 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
1245 // expanded view becomes visible on the screen. See b/126856255
1246 mExpandedViewContainer.setAlpha(0.0f);
1247 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Lyn Han3cd75d72020-02-15 19:10:12 -08001248 previouslySelected.setContentVisibility(false);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001249 updateExpandedBubble();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001250 requestUpdate();
Lyn Han3cd75d72020-02-15 19:10:12 -08001251
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001252 logBubbleEvent(previouslySelected,
1253 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
1254 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor99a302602019-06-14 11:39:56 -07001255 notifyExpansionChanged(previouslySelected, false /* expanded */);
1256 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001257 });
1258 }
1259 }
1260
1261 /**
1262 * Changes the expanded state of the stack.
1263 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001264 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001265 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001266 // via BubbleData.Listener
1267 public void setExpanded(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001268 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001269 Log.d(TAG, "setExpanded: " + shouldExpand);
1270 }
Lyn Han285ad302019-05-29 19:01:39 -07001271 if (shouldExpand == mIsExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001272 return;
1273 }
Joshua Tsujibe60a582020-03-23 17:17:26 -04001274
1275 mSysUiState
1276 .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
1277 .commitUpdate(mContext.getDisplayId());
1278
Lyn Han285ad302019-05-29 19:01:39 -07001279 if (mIsExpanded) {
1280 animateCollapse();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001281 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001282 } else {
Lyn Han285ad302019-05-29 19:01:39 -07001283 animateExpansion();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001284 // TODO: move next line to BubbleData
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001285 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
1286 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001287 }
Mady Mellor99a302602019-06-14 11:39:56 -07001288 notifyExpansionChanged(mExpandedBubble, mIsExpanded);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001289 }
1290
1291 /**
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001292 * If necessary, shows the user education view for the bubble stack. This appears the first
1293 * time a user taps on a bubble.
1294 *
1295 * @return true if user education was shown, false otherwise.
1296 */
1297 private boolean maybeShowStackUserEducation() {
1298 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001299 mUserEducationView.setAlpha(0);
1300 mUserEducationView.setVisibility(VISIBLE);
1301 // Post so we have height of mUserEducationView
1302 mUserEducationView.post(() -> {
1303 final int viewHeight = mUserEducationView.getHeight();
1304 PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
1305 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
1306 mUserEducationView.setTranslationY(translationY);
1307 mUserEducationView.animate()
1308 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1309 .setInterpolator(FAST_OUT_SLOW_IN)
1310 .alpha(1);
1311 });
1312 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
1313 return true;
1314 }
1315 return false;
1316 }
1317
1318 /**
1319 * If necessary, hides the user education view for the bubble stack.
1320 *
1321 * @param fromExpansion if true this indicates the hide is happening due to the bubble being
1322 * expanded, false if due to a touch outside of the bubble stack.
1323 */
1324 void hideStackUserEducation(boolean fromExpansion) {
1325 if (mShouldShowUserEducation
1326 && mUserEducationView.getVisibility() == VISIBLE
1327 && !mAnimatingEducationAway) {
1328 mAnimatingEducationAway = true;
1329 mUserEducationView.animate()
1330 .alpha(0)
1331 .setDuration(fromExpansion
1332 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1333 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1334 .withEndAction(() -> {
1335 mAnimatingEducationAway = false;
1336 mShouldShowUserEducation = shouldShowBubblesEducation();
1337 mUserEducationView.setVisibility(GONE);
1338 });
1339 }
1340 }
1341
1342 /**
1343 * If necessary, toggles the user education view for the manage button. This is shown when the
1344 * bubble stack is expanded for the first time.
1345 *
1346 * @param show whether the user education view should show or not.
1347 */
1348 void maybeShowManageEducation(boolean show) {
1349 if (mManageEducationView == null) {
1350 return;
1351 }
1352 if (show
1353 && mShouldShowManageEducation
1354 && mManageEducationView.getVisibility() != VISIBLE
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001355 && mIsExpanded
1356 && mExpandedBubble.getExpandedView() != null) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001357 mManageEducationView.setAlpha(0);
1358 mManageEducationView.setVisibility(VISIBLE);
1359 mManageEducationView.post(() -> {
1360 final Rect position =
1361 mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
1362 final int viewHeight = mManageEducationView.getManageViewHeight();
1363 final int inset = getResources().getDimensionPixelSize(
1364 R.dimen.bubbles_manage_education_top_inset);
1365 mManageEducationView.bringToFront();
1366 mManageEducationView.setManageViewPosition(position.left,
1367 position.top - viewHeight + inset);
1368 mManageEducationView.setPointerPosition(position.centerX() - position.left);
1369 mManageEducationView.animate()
1370 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1371 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
1372 });
1373 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
1374 } else if (!show
1375 && mManageEducationView.getVisibility() == VISIBLE
1376 && !mAnimatingManageEducationAway) {
1377 mManageEducationView.animate()
1378 .alpha(0)
1379 .setDuration(mIsExpansionAnimating
1380 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1381 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1382 .withEndAction(() -> {
1383 mAnimatingManageEducationAway = false;
1384 mShouldShowManageEducation = shouldShowManageEducation();
1385 mManageEducationView.setVisibility(GONE);
1386 });
1387 }
1388 }
1389
1390 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001391 * Dismiss the stack of bubbles.
Lyn Han1b4f25e2019-06-11 13:56:34 -07001392 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001393 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001394 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001395 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -07001396 void stackDismissed(int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001397 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001398 Log.d(TAG, "stackDismissed: reason=" + reason);
1399 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001400 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -05001401 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001402 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001403 }
1404
1405 /**
Joshua Tsujif5c6a9c2020-02-25 17:47:59 -05001406 * @deprecated use {@link #setExpanded(boolean)} and
1407 * {@link BubbleData#setSelectedBubble(Bubble)}
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001408 */
1409 @Deprecated
1410 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -08001411 void collapseStack(Runnable endRunnable) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001412 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001413 Log.d(TAG, "collapseStack(endRunnable)");
1414 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001415 mBubbleData.setExpanded(false);
Mady Mellor9801e852019-01-22 14:50:28 -08001416 // TODO - use the runnable at end of animation
1417 endRunnable.run();
1418 }
1419
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001420 void showExpandedViewContents(int displayId) {
1421 if (mExpandedBubble != null
Mady Melloradd5c6a92020-03-31 17:22:48 -07001422 && mExpandedBubble.getExpandedView() != null
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001423 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
1424 mExpandedBubble.setContentVisibility(true);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001425 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001426 }
1427
Lyn Han285ad302019-05-29 19:01:39 -07001428 private void beforeExpandedViewAnimation() {
1429 hideFlyoutImmediate();
1430 updateExpandedBubble();
1431 updateExpandedView();
1432 mIsExpansionAnimating = true;
1433 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001434
Lyn Han285ad302019-05-29 19:01:39 -07001435 private void afterExpandedViewAnimation() {
1436 updateExpandedView();
1437 mIsExpansionAnimating = false;
1438 requestUpdate();
1439 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001440
Lyn Han285ad302019-05-29 19:01:39 -07001441 private void animateCollapse() {
1442 mIsExpanded = false;
Lyn Han3cd75d72020-02-15 19:10:12 -08001443 final BubbleViewProvider previouslySelected = mExpandedBubble;
Lyn Han285ad302019-05-29 19:01:39 -07001444 beforeExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001445 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001446
Lyn Hanb58c7562020-01-07 14:29:20 -08001447 if (DEBUG_BUBBLE_STACK_VIEW) {
1448 Log.d(TAG, "animateCollapse");
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001449 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
Lyn Han9f66c3b2020-03-05 23:59:29 -08001450 mExpandedBubble));
Lyn Hanb58c7562020-01-07 14:29:20 -08001451 }
1452 updateOverflowBtnVisibility(/* apply */ false);
Lyn Han285ad302019-05-29 19:01:39 -07001453 mBubbleContainer.cancelAllAnimations();
1454 mExpandedAnimationController.collapseBackToStack(
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001455 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
1456 /* collapseTo */,
Lyn Han285ad302019-05-29 19:01:39 -07001457 () -> {
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001458 mBubbleContainer.setActiveController(mStackAnimationController);
Lyn Han285ad302019-05-29 19:01:39 -07001459 afterExpandedViewAnimation();
Lyn Han3cd75d72020-02-15 19:10:12 -08001460 previouslySelected.setContentVisibility(false);
Lyn Han285ad302019-05-29 19:01:39 -07001461 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001462
Lyn Han285ad302019-05-29 19:01:39 -07001463 mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
1464 mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
1465 mExpandedViewContainer.animate()
1466 .setDuration(100)
1467 .alpha(0f);
1468 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001469
Lyn Han285ad302019-05-29 19:01:39 -07001470 private void animateExpansion() {
1471 mIsExpanded = true;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001472 hideStackUserEducation(true /* fromExpansion */);
Lyn Han285ad302019-05-29 19:01:39 -07001473 beforeExpandedViewAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001474
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001475 mBubbleContainer.setActiveController(mExpandedAnimationController);
Lyn Hanb58c7562020-01-07 14:29:20 -08001476 updateOverflowBtnVisibility(/* apply */ false);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001477 mExpandedAnimationController.expandFromStack(() -> {
1478 updatePointerPosition();
1479 afterExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001480 maybeShowManageEducation(true);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001481 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001482
Lyn Han285ad302019-05-29 19:01:39 -07001483 mExpandedViewContainer.setTranslationX(getCollapsedX());
1484 mExpandedViewContainer.setTranslationY(getCollapsedY());
1485 mExpandedViewContainer.setAlpha(0f);
1486
1487 mExpandedViewXAnim.animateToFinalPosition(0f);
1488 mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1489 mExpandedViewContainer.animate()
1490 .setDuration(100)
1491 .alpha(1f);
1492 }
1493
1494 private float getCollapsedX() {
1495 return mStackAnimationController.getStackPosition().x < getWidth() / 2
1496 ? -mExpandedAnimateXDistance
1497 : mExpandedAnimateXDistance;
1498 }
1499
1500 private float getCollapsedY() {
1501 return Math.min(mStackAnimationController.getStackPosition().y,
1502 mExpandedAnimateYDistance);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001503 }
1504
Lyn Han3cd75d72020-02-15 19:10:12 -08001505 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
Mady Mellor99a302602019-06-14 11:39:56 -07001506 if (mExpandListener != null && bubble != null) {
1507 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001508 }
1509 }
1510
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001511 /** Return the BubbleView at the given index from the bubble container. */
Mady Mellorb8aaf972019-11-26 10:28:00 -08001512 public BadgedImageView getBubbleAt(int i) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001513 return getBubbleCount() > i
Mady Mellorb8aaf972019-11-26 10:28:00 -08001514 ? (BadgedImageView) mBubbleContainer.getChildAt(i)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001515 : null;
1516 }
1517
Joshua Tsujia19515f2019-02-13 18:02:29 -05001518 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
1519 public void onImeVisibilityChanged(boolean visible, int height) {
Mady Mellordf611cf2019-08-21 17:28:49 -07001520 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
Joshua Tsuji4b395912019-04-19 17:18:40 -04001521
Joshua Tsujiff6b0f22020-03-09 14:55:19 -04001522 if (!mIsExpanded && getBubbleCount() > 0) {
1523 final float stackDestinationY =
1524 mStackAnimationController.animateForImeVisibility(visible);
1525
1526 // How far the stack is animating due to IME, we'll just animate the flyout by that
1527 // much too.
1528 final float stackDy =
1529 stackDestinationY - mStackAnimationController.getStackPosition().y;
1530
1531 // If the flyout is visible, translate it along with the bubble stack.
1532 if (mFlyout.getVisibility() == VISIBLE) {
1533 PhysicsAnimator.getInstance(mFlyout)
1534 .spring(DynamicAnimation.TRANSLATION_Y,
1535 mFlyout.getTranslationY() + stackDy,
1536 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
1537 .start();
1538 }
Joshua Tsujia19515f2019-02-13 18:02:29 -05001539 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001540 }
1541
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001542 /**
1543 * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a
1544 * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV).
1545 * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided
1546 * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to
1547 * the special nature of ActivityView, it does not respect the standard
1548 * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for
1549 * this purpose.
1550 *
1551 * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation
1552 * properties for performance reasons. This means that the default implementation of this method
1553 * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in
1554 * it not receiving any touch events. This was previously addressed by returning false in the
1555 * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any
1556 * touch handlers in the stack or its child views.
1557 *
1558 * To support touch handlers, we're overriding this method to leave the ActivityView's touchable
1559 * region alone. The only touchable part of the stack that can ever overlap the AV is a
1560 * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually
1561 * updating the touchable region to allow users to grab a bubble while it completes its ~50ms
1562 * animation back to the bubble row.
1563 *
1564 * NOTE: Any future additions to the stack that obscure the ActivityView region will need their
1565 * bounds subtracted here in order to receive touch events.
1566 */
1567 @Override
1568 public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
Joshua Tsujiba9fef02020-04-09 17:40:35 -04001569 // If the notification shade is expanded, we shouldn't let the ActivityView steal any touch
1570 // events from any location.
1571 if (mNotificationShadeWindowController.getPanelExpanded()) {
1572 touchableRegion.setEmpty();
1573 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001574 }
1575
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001576 /**
1577 * If you're here because you're not receiving touch events on a view that is a descendant of
1578 * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the
1579 * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView
1580 * consumes all touch events within its bounds, even for views like the BubbleStackView that are
1581 * above it. It ignores typical view touch handling methods like this one and
1582 * dispatchTouchEvent.
1583 */
1584 @Override
1585 public boolean onInterceptTouchEvent(MotionEvent ev) {
1586 return super.onInterceptTouchEvent(ev);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001587 }
1588
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001589 @Override
1590 public boolean dispatchTouchEvent(MotionEvent ev) {
1591 boolean dispatched = super.dispatchTouchEvent(ev);
1592
1593 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned
1594 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will
1595 // then be passed to the new bubble, which will not consume them since it hasn't received an
1596 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler
1597 // until the current gesture ends with an ACTION_UP event.
1598 if (!dispatched && !mIsExpanded && mIsGestureInProgress) {
1599 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev);
Joshua Tsuji442b6272019-02-08 13:23:43 -05001600 }
1601
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001602 mIsGestureInProgress =
1603 ev.getAction() != MotionEvent.ACTION_UP
1604 && ev.getAction() != MotionEvent.ACTION_CANCEL;
1605
1606 return dispatched;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001607 }
1608
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001609 void setFlyoutStateForDragLength(float deltaX) {
Joshua Tsuji8e05aab2019-08-22 14:57:50 -04001610 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1611 // is continually called.
1612 if (mFlyout.getWidth() <= 0) {
1613 return;
1614 }
1615
Joshua Tsuji6549e702019-05-02 13:13:16 -04001616 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1617 mFlyoutDragDeltaX = deltaX;
1618
1619 final float collapsePercent =
1620 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1621 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1622
Lyn Han61d5d562019-07-01 17:39:38 -07001623 // Calculate how to translate the flyout if it has been dragged too far in either direction.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001624 float overscrollTranslation = 0f;
1625 if (collapsePercent < 0f || collapsePercent > 1f) {
1626 // Whether we are more than 100% transitioned to the dot.
1627 final boolean overscrollingPastDot = collapsePercent > 1f;
1628
1629 // Whether we are overscrolling physically to the left - this can either be pulling the
1630 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1631 // after it has already become the dot.
1632 final boolean overscrollingLeft =
1633 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001634 overscrollTranslation =
1635 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1636 * (overscrollingLeft ? -1 : 1)
1637 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001638 // Attenuate the smaller dot less than the larger flyout.
1639 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001640 }
1641
1642 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1643 }
1644
Joshua Tsuji20103542020-02-18 14:06:28 -05001645 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001646 private boolean passEventToMagnetizedObject(MotionEvent event) {
Joshua Tsuji20103542020-02-18 14:06:28 -05001647 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
1648 }
1649
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001650 /**
1651 * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the
1652 * stack, if we're collapsed.
1653 */
1654 private void dismissMagnetizedObject() {
1655 if (mIsExpanded) {
1656 final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
1657 final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView);
1658
1659 if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) {
1660 mBubbleData.notificationEntryRemoved(
1661 draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
1662 }
1663 } else {
1664 mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
1665 }
1666 }
1667
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001668 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1669 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1670 mDesaturateAndDarkenTargetView = targetView;
1671
1672 if (desaturateAndDarken) {
1673 // Use the animated paint for the bubbles.
1674 mDesaturateAndDarkenTargetView.setLayerType(
1675 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1676 mDesaturateAndDarkenAnimator.removeAllListeners();
1677 mDesaturateAndDarkenAnimator.start();
1678 } else {
1679 mDesaturateAndDarkenAnimator.removeAllListeners();
1680 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1681 @Override
1682 public void onAnimationEnd(Animator animation) {
1683 super.onAnimationEnd(animation);
1684 // Stop using the animated paint.
1685 resetDesaturationAndDarken();
1686 }
1687 });
1688 mDesaturateAndDarkenAnimator.reverse();
1689 }
1690 }
1691
1692 private void resetDesaturationAndDarken() {
1693 mDesaturateAndDarkenAnimator.removeAllListeners();
1694 mDesaturateAndDarkenAnimator.cancel();
1695 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1696 }
1697
Lyn Han634483c2019-06-28 16:52:47 -07001698 /** Animates in the dismiss target. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001699 private void springInDismissTargetMaybe() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001700 if (mShowingDismiss) {
1701 return;
1702 }
1703
1704 mShowingDismiss = true;
1705
Joshua Tsuji20103542020-02-18 14:06:28 -05001706 mDismissTargetContainer.bringToFront();
1707 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
1708 mDismissTargetContainer.setVisibility(VISIBLE);
1709
1710 mDismissTargetAnimator.cancel();
1711 mDismissTargetAnimator
1712 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
1713 .start();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001714 }
1715
1716 /**
1717 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1718 * were dragged into the target and encircled.
1719 */
Lyn Han634483c2019-06-28 16:52:47 -07001720 private void hideDismissTarget() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001721 if (!mShowingDismiss) {
1722 return;
1723 }
1724
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001725 mShowingDismiss = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001726
Joshua Tsuji20103542020-02-18 14:06:28 -05001727 mDismissTargetAnimator
1728 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
1729 mDismissTargetSpring)
1730 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
1731 .start();
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001732 }
1733
Joshua Tsuji6549e702019-05-02 13:13:16 -04001734 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1735 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1736 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001737 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1738 // faster.
1739 mFlyoutTransitionSpring.getSpring().setStiffness(
1740 (mBubbleToExpandAfterFlyoutCollapse != null)
1741 ? SpringForce.STIFFNESS_MEDIUM
1742 : SpringForce.STIFFNESS_LOW);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001743 mFlyoutTransitionSpring
1744 .setStartValue(mFlyoutDragDeltaX)
1745 .setStartVelocity(velX)
1746 .animateToFinalPosition(collapsed
1747 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1748 : 0f);
1749 }
1750
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001751 /**
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001752 * Calculates the y position of the expanded view when it is expanded.
1753 */
Lyn Han285ad302019-05-29 19:01:39 -07001754 float getExpandedViewY() {
Lyn Han4a8efe32019-05-30 09:43:27 -07001755 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001756 }
1757
1758 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001759 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1760 */
1761 @VisibleForTesting
1762 void animateInFlyoutForBubble(Bubble bubble) {
Mady Mellordf898fd2020-01-09 09:26:36 -08001763 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001764 final BadgedImageView bubbleView = bubble.getIconView();
Mady Mellordf898fd2020-01-09 09:26:36 -08001765 if (flyoutMessage == null
1766 || flyoutMessage.message == null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001767 || !bubble.showFlyout()
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001768 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
Mark Renoufc19b4732019-06-26 12:08:33 -04001769 || isExpanded()
1770 || mIsExpansionAnimating
Joshua Tsuji14e68552019-06-06 17:17:08 -04001771 || mIsGestureInProgress
Lyn Hanf1f2c332019-08-23 17:06:56 -07001772 || mBubbleToExpandAfterFlyoutCollapse != null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001773 || bubbleView == null) {
1774 if (bubbleView != null) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001775 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001776 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001777 // Skip the message if none exists, we're expanded or animating expansion, or we're
Lyn Hanf1f2c332019-08-23 17:06:56 -07001778 // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
Mark Renoufc19b4732019-06-26 12:08:33 -04001779 return;
1780 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001781
Lyn Hanf1f2c332019-08-23 17:06:56 -07001782 mFlyoutDragDeltaX = 0f;
1783 clearFlyoutOnHide();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001784 mAfterFlyoutHidden = () -> {
1785 // Null it out to ensure it runs once.
1786 mAfterFlyoutHidden = null;
1787
1788 if (mBubbleToExpandAfterFlyoutCollapse != null) {
1789 // User tapped on the flyout and we should expand
1790 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1791 mBubbleData.setExpanded(true);
1792 mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001793 }
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001794
1795 // Stop suppressing the dot now that the flyout has morphed into the dot.
1796 bubbleView.removeDotSuppressionFlag(
1797 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Lyn Hanf1f2c332019-08-23 17:06:56 -07001798 };
1799 mFlyout.setVisibility(INVISIBLE);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001800
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001801 // Suppress the dot when we are animating the flyout.
1802 bubbleView.addDotSuppressionFlag(
1803 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001804
Lyn Hanf1f2c332019-08-23 17:06:56 -07001805 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1806 post(() -> {
1807 // An auto-expanding bubble could have been posted during the time it takes to
1808 // layout.
1809 if (isExpanded()) {
1810 return;
1811 }
1812 final Runnable expandFlyoutAfterDelay = () -> {
1813 mAnimateInFlyout = () -> {
1814 mFlyout.setVisibility(VISIBLE);
1815 mFlyoutDragDeltaX =
1816 mStackAnimationController.isStackOnLeftSide()
1817 ? -mFlyout.getWidth()
1818 : mFlyout.getWidth();
1819 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1820 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001821 };
Lyn Hanf1f2c332019-08-23 17:06:56 -07001822 mFlyout.postDelayed(mAnimateInFlyout, 200);
1823 };
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001824
1825 if (bubble.getIconView() == null) {
1826 return;
1827 }
1828
Mady Mellordf898fd2020-01-09 09:26:36 -08001829 mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
1830 mStackAnimationController.getStackPosition(), getWidth(),
Lyn Hanf1f2c332019-08-23 17:06:56 -07001831 mStackAnimationController.isStackOnLeftSide(),
Mady Mellor05e860b2019-10-30 22:48:15 -07001832 bubble.getIconView().getDotColor() /* dotColor */,
Lyn Hanf1f2c332019-08-23 17:06:56 -07001833 expandFlyoutAfterDelay /* onLayoutComplete */,
Mady Mellorb8aaf972019-11-26 10:28:00 -08001834 mAfterFlyoutHidden,
1835 bubble.getIconView().getDotCenter(),
1836 !bubble.showDot());
Lyn Hanf1f2c332019-08-23 17:06:56 -07001837 mFlyout.bringToFront();
1838 });
Mark Renoufc19b4732019-06-26 12:08:33 -04001839 mFlyout.removeCallbacks(mHideFlyout);
1840 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001841 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001842 }
1843
1844 /** Hide the flyout immediately and cancel any pending hide runnables. */
1845 private void hideFlyoutImmediate() {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001846 clearFlyoutOnHide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001847 mFlyout.removeCallbacks(mAnimateInFlyout);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001848 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001849 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001850 }
1851
Lyn Hanf1f2c332019-08-23 17:06:56 -07001852 private void clearFlyoutOnHide() {
1853 mFlyout.removeCallbacks(mAnimateInFlyout);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001854 if (mAfterFlyoutHidden == null) {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001855 return;
1856 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001857 mAfterFlyoutHidden.run();
1858 mAfterFlyoutHidden = null;
Lyn Hanf1f2c332019-08-23 17:06:56 -07001859 }
1860
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001861 @Override
1862 public void getBoundsOnScreen(Rect outRect) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001863 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1864 // When user education shows then capture all touches
1865 outRect.set(0, 0, getWidth(), getHeight());
1866 return;
1867 }
1868
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001869 if (!mIsExpanded) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001870 if (getBubbleCount() > 0) {
Mady Mellor217b2e92019-02-27 11:44:16 -08001871 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1872 }
Mady Mellore9371bc2019-07-10 18:50:59 -07001873 // Increase the touch target size of the bubble
1874 outRect.top -= mBubbleTouchPadding;
1875 outRect.left -= mBubbleTouchPadding;
1876 outRect.right += mBubbleTouchPadding;
1877 outRect.bottom += mBubbleTouchPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001878 } else {
1879 mBubbleContainer.getBoundsOnScreen(outRect);
1880 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001881
Joshua Tsuji6549e702019-05-02 13:13:16 -04001882 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001883 final Rect flyoutBounds = new Rect();
1884 mFlyout.getBoundsOnScreen(flyoutBounds);
1885 outRect.union(flyoutBounds);
1886 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001887 }
1888
1889 private int getStatusBarHeight() {
1890 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001891 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001892 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05001893 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001894 insets.getDisplayCutout() != null
1895 ? insets.getDisplayCutout().getSafeInsetTop()
1896 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001897 }
1898
1899 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001900 }
1901
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001902 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07001903 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001904 return;
1905 }
1906 mViewUpdatedRequested = true;
1907 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1908 invalidate();
1909 }
1910
1911 private void updateExpandedBubble() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001912 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001913 Log.d(TAG, "updateExpandedBubble()");
1914 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001915 mExpandedViewContainer.removeAllViews();
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001916 if (mIsExpanded && mExpandedBubble != null
1917 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -08001918 BubbleExpandedView bev = mExpandedBubble.getExpandedView();
Lyn Hana0bb02e2020-01-28 17:57:27 -08001919 mExpandedViewContainer.addView(bev);
1920 bev.populateExpandedView();
1921 mExpandedViewContainer.setVisibility(VISIBLE);
Issei Suzukic0387542019-03-08 17:31:14 +01001922 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001923 }
1924 }
1925
Lyn Han285ad302019-05-29 19:01:39 -07001926 private void updateExpandedView() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001927 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Han285ad302019-05-29 19:01:39 -07001928 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001929 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001930
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001931 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08001932 if (mIsExpanded) {
Lyn Han285ad302019-05-29 19:01:39 -07001933 final float y = getExpandedViewY();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001934 if (!mExpandedViewYAnim.isRunning()) {
1935 // We're not animating so set the value
1936 mExpandedViewContainer.setTranslationY(y);
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001937 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001938 mExpandedBubble.getExpandedView().updateView();
1939 }
Mady Mellor5d8f1402019-02-21 18:23:52 -08001940 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07001941 // We are animating so update the value; there is an end listener on the animator
1942 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08001943 mExpandedViewYAnim.animateToFinalPosition(y);
1944 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001945 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001946
Joshua Tsuji6549e702019-05-02 13:13:16 -04001947 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001948 updateBubbleZOrdersAndDotPosition(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001949 }
1950
1951 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001952 private void updateBubbleZOrdersAndDotPosition(boolean animate) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001953 int bubbleCount = getBubbleCount();
Lyn Han1b4f25e2019-06-11 13:56:34 -07001954 for (int i = 0; i < bubbleCount; i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001955 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellor70958542019-09-24 17:12:46 -07001956 bv.setZ((mMaxBubbles * mBubbleElevation) - i);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001957
Joshua Tsuji6549e702019-05-02 13:13:16 -04001958 // If the dot is on the left, and so is the stack, we need to change the dot position.
1959 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001960 bv.setDotPositionOnLeft(!mStackOnLeftOrWillBe, animate);
1961 }
1962
1963 if (!mIsExpanded && i > 0) {
1964 // If we're collapsed and this bubble is behind other bubbles, suppress its dot.
1965 bv.addDotSuppressionFlag(
1966 BadgedImageView.SuppressionFlag.BEHIND_STACK);
1967 } else {
1968 bv.removeDotSuppressionFlag(
1969 BadgedImageView.SuppressionFlag.BEHIND_STACK);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001970 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001971 }
1972 }
1973
Mady Mellorde2d4d22019-01-29 14:15:34 -08001974 private void updatePointerPosition() {
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001975 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
Lyn Han522e9ff2019-05-17 13:26:13 -07001976 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08001977 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001978 int index = getBubbleIndex(mExpandedBubble);
Lyn Hanf44562b2020-03-30 16:40:46 -07001979 if (index == -1) {
1980 return;
1981 }
Lyn Han522e9ff2019-05-17 13:26:13 -07001982 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1983 float halfBubble = mBubbleSize / 2f;
Mady Mellor9be3bed2019-08-21 17:26:26 -07001984 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
1985 // Padding might be adjusted for insets, so get it directly from the view
1986 bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
Lyn Han9f66c3b2020-03-05 23:59:29 -08001987 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08001988 }
1989
Steven Wua254dab2019-01-29 11:30:39 -05001990 /**
1991 * @return the number of bubbles in the stack view.
1992 */
Steven Wub00225b2019-02-08 14:27:42 -05001993 public int getBubbleCount() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001994 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1995 // Subtract 1 for the overflow button that is always in the bubble container.
1996 return mBubbleContainer.getChildCount() - 1;
1997 }
1998 return mBubbleContainer.getChildCount();
Steven Wua254dab2019-01-29 11:30:39 -05001999 }
2000
2001 /**
2002 * Finds the bubble index within the stack.
2003 *
Lyn Han3cd75d72020-02-15 19:10:12 -08002004 * @param provider the bubble view provider with the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05002005 * @return the index of the bubble view within the bubble stack. The range of the position
2006 * is between 0 and the bubble count minus 1.
2007 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002008 int getBubbleIndex(@Nullable BubbleViewProvider provider) {
Lyn Han9f66c3b2020-03-05 23:59:29 -08002009 if (provider == null) {
Steven Wua62cb6a2019-02-15 17:12:51 -05002010 return 0;
2011 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08002012 return mBubbleContainer.indexOfChild(provider.getIconView());
Steven Wua254dab2019-01-29 11:30:39 -05002013 }
2014
2015 /**
2016 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
2017 */
Steven Wub00225b2019-02-08 14:27:42 -05002018 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002019 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05002020 .setScale(4, RoundingMode.CEILING.HALF_UP)
2021 .floatValue();
2022 }
2023
2024 /**
2025 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
2026 */
Steven Wub00225b2019-02-08 14:27:42 -05002027 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002028 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05002029 .setScale(4, RoundingMode.CEILING.HALF_UP)
2030 .floatValue();
2031 }
2032
Joshua Tsujia19515f2019-02-13 18:02:29 -05002033 public PointF getStackPosition() {
2034 return mStackAnimationController.getStackPosition();
2035 }
2036
Steven Wua254dab2019-01-29 11:30:39 -05002037 /**
2038 * Logs the bubble UI event.
2039 *
2040 * @param bubble the bubble that is being interacted on. Null value indicates that
2041 * the user interaction is not specific to one bubble.
2042 * @param action the user interaction enum.
2043 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002044 private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
2045 if (bubble == null) {
2046 return;
Steven Wua254dab2019-01-29 11:30:39 -05002047 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002048 bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
2049 getNormalizedYPosition(), getBubbleIndex(bubble));
Steven Wua254dab2019-01-29 11:30:39 -05002050 }
Mark Renouf041d7262019-02-06 12:09:41 -05002051
2052 /**
2053 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
2054 * a back key down/up event pair is forwarded to the bubble Activity.
2055 */
2056 boolean performBackPressIfNeeded() {
Mady Mellor2dce0fe2020-04-10 13:40:05 -07002057 if (!isExpanded() || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
Mark Renouf041d7262019-02-06 12:09:41 -05002058 return false;
2059 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002060 return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
Mark Renouf041d7262019-02-06 12:09:41 -05002061 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002062
Mady Mellor5a3e94b2020-02-07 12:16:21 -08002063 /** Whether the educational view should appear for bubbles. **/
2064 private boolean shouldShowBubblesEducation() {
2065 return BubbleDebugConfig.forceShowUserEducation(getContext())
2066 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
2067 }
2068
2069 /** Whether the educational view should appear for the expanded view "manage" button. **/
2070 private boolean shouldShowManageEducation() {
2071 return BubbleDebugConfig.forceShowUserEducation(getContext())
2072 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
2073 }
2074
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002075 /** For debugging only */
2076 List<Bubble> getBubblesOnScreen() {
2077 List<Bubble> bubbles = new ArrayList<>();
Lyn Hanc47e1712020-01-28 21:43:34 -08002078 for (int i = 0; i < getBubbleCount(); i++) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002079 View child = mBubbleContainer.getChildAt(i);
Mady Mellorb8aaf972019-11-26 10:28:00 -08002080 if (child instanceof BadgedImageView) {
2081 String key = ((BadgedImageView) child).getKey();
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002082 Bubble bubble = mBubbleData.getBubbleWithKey(key);
2083 bubbles.add(bubble);
2084 }
2085 }
2086 return bubbles;
2087 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002088}