blob: 6ba1aa80ac75a8bbe8c0663cd6e521cd206ea56d [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;
Joshua Tsuji4f8eea22020-05-21 00:16:06 -040034import android.content.ContentResolver;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080035import android.content.Context;
Joshua Tsuji6855cab2020-04-16 01:05:39 -040036import android.content.Intent;
Lyn Hanf4730312019-06-18 11:18:58 -070037import android.content.res.Configuration;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080038import android.content.res.Resources;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080039import android.content.res.TypedArray;
40import android.graphics.Color;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040041import android.graphics.ColorMatrix;
42import android.graphics.ColorMatrixColorFilter;
Joshua Tsuji6855cab2020-04-16 01:05:39 -040043import android.graphics.Outline;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040044import android.graphics.Paint;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080045import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080046import android.graphics.PointF;
47import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080048import android.graphics.RectF;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040049import android.graphics.Region;
Mady Mellor217b2e92019-02-27 11:44:16 -080050import android.os.Bundle;
Joshua Tsuji4f8eea22020-05-21 00:16:06 -040051import android.provider.Settings;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050052import android.util.Log;
Issei Suzukic0387542019-03-08 17:31:14 +010053import android.view.Choreographer;
Mady Mellor9be3bed2019-08-21 17:26:26 -070054import android.view.DisplayCutout;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -040055import android.view.Gravity;
Mady Mellordea7ecf2018-12-10 15:47:40 -080056import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080057import android.view.MotionEvent;
58import android.view.View;
Joshua Tsuji20103542020-02-18 14:06:28 -050059import android.view.ViewGroup;
Joshua Tsuji6855cab2020-04-16 01:05:39 -040060import android.view.ViewOutlineProvider;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080061import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050062import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080063import android.view.WindowManager;
Mady Mellor217b2e92019-02-27 11:44:16 -080064import android.view.accessibility.AccessibilityNodeInfo;
Lyn Hane68d0912019-05-02 18:28:01 -070065import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040066import android.view.animation.AccelerateDecelerateInterpolator;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080067import android.widget.FrameLayout;
Joshua Tsuji6855cab2020-04-16 01:05:39 -040068import android.widget.ImageView;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080069import android.widget.TextView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080070
Mark Renoufcecc77b2019-01-30 16:32:24 -050071import androidx.annotation.MainThread;
Joshua Tsuji20103542020-02-18 14:06:28 -050072import androidx.annotation.NonNull;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080073import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080074import androidx.dynamicanimation.animation.DynamicAnimation;
Joshua Tsuji6549e702019-05-02 13:13:16 -040075import androidx.dynamicanimation.animation.FloatPropertyCompat;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080076import androidx.dynamicanimation.animation.SpringAnimation;
77import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080078
Mady Melloredd4ee12019-01-18 10:45:11 -080079import com.android.internal.annotations.VisibleForTesting;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080080import com.android.internal.util.ContrastColorUtil;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080081import com.android.internal.widget.ViewClippingUtil;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080082import com.android.systemui.Prefs;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080083import com.android.systemui.R;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080084import com.android.systemui.bubbles.animation.ExpandedAnimationController;
85import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
86import com.android.systemui.bubbles.animation.StackAnimationController;
Joshua Tsujibe60a582020-03-23 17:17:26 -040087import com.android.systemui.model.SysUiState;
88import com.android.systemui.shared.system.QuickStepContract;
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -080089import com.android.systemui.shared.system.SysUiStatsLog;
Joshua Tsuji6855cab2020-04-16 01:05:39 -040090import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Joshua Tsujiba9fef02020-04-09 17:40:35 -040091import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
Joshua Tsuji20103542020-02-18 14:06:28 -050092import com.android.systemui.util.DismissCircleView;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050093import com.android.systemui.util.FloatingContentCoordinator;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040094import com.android.systemui.util.RelativeTouchListener;
Joshua Tsuji20103542020-02-18 14:06:28 -050095import com.android.systemui.util.animation.PhysicsAnimator;
96import com.android.systemui.util.magnetictarget.MagnetizedObject;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080097
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040098import java.io.FileDescriptor;
99import java.io.PrintWriter;
Steven Wua254dab2019-01-29 11:30:39 -0500100import java.math.BigDecimal;
101import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400102import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -0400103import java.util.Collections;
104import java.util.List;
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400105import java.util.function.Consumer;
Steven Wua254dab2019-01-29 11:30:39 -0500106
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800107/**
108 * Renders bubbles in a stack and handles animating expanded and collapsed states.
109 */
Joshua Tsujid9923e52020-04-29 15:33:01 -0400110public class BubbleStackView extends FrameLayout
111 implements ViewTreeObserver.OnComputeInternalInsetsListener {
Issei Suzukia8d07312019-06-07 12:56:19 +0200112 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800113
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800114 /** Animation durations for bubble stack user education views. **/
115 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
116 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
117
Joshua Tsuji6549e702019-05-02 13:13:16 -0400118 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
119 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
120
121 /** Velocity required to dismiss the flyout via drag. */
122 private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
123
124 /**
125 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
126 * for every 8 pixels overscrolled).
127 */
128 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
129
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400130 /** Duration of the flyout alpha animations. */
131 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
132
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400133 /** Percent to darken the bubbles when they're in the dismiss target. */
134 private static final float DARKEN_PERCENT = 0.3f;
135
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400136 /** How long to wait, in milliseconds, before hiding the flyout. */
137 @VisibleForTesting
138 static final int FLYOUT_HIDE_AFTER = 5000;
139
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400140 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
141 new PhysicsAnimator.SpringConfig(
142 StackAnimationController.IME_ANIMATION_STIFFNESS,
143 StackAnimationController.DEFAULT_BOUNCINESS);
144
Issei Suzukic0387542019-03-08 17:31:14 +0100145 /**
146 * Interface to synchronize {@link View} state and the screen.
147 *
148 * {@hide}
149 */
150 interface SurfaceSynchronizer {
151 /**
152 * Wait until requested change on a {@link View} is reflected on the screen.
153 *
154 * @param callback callback to run after the change is reflected on the screen.
155 */
156 void syncSurfaceAndRun(Runnable callback);
157 }
158
159 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
160 new SurfaceSynchronizer() {
161 @Override
162 public void syncSurfaceAndRun(Runnable callback) {
163 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
164 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
165 // the requested change is reflected on the screen.
166 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
167 // surfaces, rewrite this logic with them.
168 private int mFrameWait = 2;
169
170 @Override
171 public void doFrame(long frameTimeNanos) {
172 if (--mFrameWait > 0) {
173 Choreographer.getInstance().postFrameCallback(this);
174 } else {
175 callback.run();
176 }
177 }
178 });
179 }
180 };
181
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800182 private Point mDisplaySize;
183
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800184 private final SpringAnimation mExpandedViewXAnim;
185 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800186 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800187
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400188 private final ValueAnimator mDesaturateAndDarkenAnimator;
189 private final Paint mDesaturateAndDarkenPaint = new Paint();
190
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800191 private PhysicsAnimationLayout mBubbleContainer;
192 private StackAnimationController mStackAnimationController;
193 private ExpandedAnimationController mExpandedAnimationController;
194
Mady Mellor3dff9e62019-02-05 18:12:53 -0800195 private FrameLayout mExpandedViewContainer;
196
Joshua Tsuji6549e702019-05-02 13:13:16 -0400197 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400198 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400199 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700200 /**
201 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
202 * previous one animates out.
203 */
Mady Mellorb8aaf972019-11-26 10:28:00 -0800204 private Runnable mAfterFlyoutHidden;
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800205 /**
206 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
207 * once it collapses.
208 */
209 @Nullable
210 private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400211
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400212 /** Layout change listener that moves the stack to the nearest valid position on rotation. */
Lyn Hanf4730312019-06-18 11:18:58 -0700213 private OnLayoutChangeListener mOrientationChangedListener;
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400214 /** Whether the stack was on the left side of the screen prior to rotation. */
215 private boolean mWasOnLeftBeforeRotation = false;
216 /**
217 * How far down the screen the stack was before rotation, in terms of percentage of the way down
218 * the allowable region. Defaults to -1 if not set.
219 */
220 private float mVerticalPosPercentBeforeRotation = -1;
221
Mady Mellor70958542019-09-24 17:12:46 -0700222 private int mMaxBubbles;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800223 private int mBubbleSize;
Mady Mellor70958542019-09-24 17:12:46 -0700224 private int mBubbleElevation;
Lyn Han4a8efe32019-05-30 09:43:27 -0700225 private int mBubblePaddingTop;
Mady Mellore9371bc2019-07-10 18:50:59 -0700226 private int mBubbleTouchPadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700227 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800228 private int mExpandedAnimateXDistance;
229 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700230 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500231 private int mStatusBarHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500232 private int mImeOffset;
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400233 @Nullable private BubbleViewProvider mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800234 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800235
Joshua Tsuji6549e702019-05-02 13:13:16 -0400236 /** Whether the stack is currently on the left side of the screen, or animating there. */
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400237 private boolean mStackOnLeftOrWillBe = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400238
239 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
240 private boolean mIsGestureInProgress = false;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400241
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400242 /** Description of current animation controller state. */
243 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
244 pw.println("Stack view state:");
245 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
246 pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
247 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400248 mStackAnimationController.dump(fd, pw, args);
249 mExpandedAnimationController.dump(fd, pw, args);
250 }
251
Mady Mellorcd9b1302018-11-06 18:08:04 -0800252 private BubbleController.BubbleExpandListener mExpandListener;
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400253
254 /** Callback to run when we want to unbubble the given notification's conversation. */
255 private Consumer<NotificationEntry> mUnbubbleConversationCallback;
256
Joshua Tsujibe60a582020-03-23 17:17:26 -0400257 private SysUiState mSysUiState;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800258
259 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700260 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400261 private boolean mShowingDismiss = false;
262
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400263 /** The view to desaturate/darken when magneted to the dismiss target. */
264 private View mDesaturateAndDarkenTargetView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800265
Mady Mellor3dff9e62019-02-05 18:12:53 -0800266 private LayoutInflater mInflater;
267
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400268 private Rect mTempRect = new Rect();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800269
Mark Renouf821e6782019-04-01 14:17:37 -0400270 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
271
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800272 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
273 new ViewTreeObserver.OnPreDrawListener() {
274 @Override
275 public boolean onPreDraw() {
276 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Lyn Han285ad302019-05-29 19:01:39 -0700277 updateExpandedView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800278 mViewUpdatedRequested = false;
279 return true;
280 }
281 };
282
Mark Renouf821e6782019-04-01 14:17:37 -0400283 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
284 this::updateSystemGestureExcludeRects;
285
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800286 private ViewClippingUtil.ClippingParameters mClippingParameters =
287 new ViewClippingUtil.ClippingParameters() {
288
Lyn Han522e9ff2019-05-17 13:26:13 -0700289 @Override
290 public boolean shouldFinish(View view) {
291 return false;
292 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800293
Lyn Han522e9ff2019-05-17 13:26:13 -0700294 @Override
295 public boolean isClippingEnablingAllowed(View view) {
296 return !mIsExpanded;
297 }
298 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800299
Joshua Tsuji6549e702019-05-02 13:13:16 -0400300 /** Float property that 'drags' the flyout. */
301 private final FloatPropertyCompat mFlyoutCollapseProperty =
302 new FloatPropertyCompat("FlyoutCollapseSpring") {
303 @Override
304 public float getValue(Object o) {
305 return mFlyoutDragDeltaX;
306 }
307
308 @Override
309 public void setValue(Object o, float v) {
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400310 setFlyoutStateForDragLength(v);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400311 }
312 };
313
314 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
315 private final SpringAnimation mFlyoutTransitionSpring =
316 new SpringAnimation(this, mFlyoutCollapseProperty);
317
318 /** Distance the flyout has been dragged in the X axis. */
319 private float mFlyoutDragDeltaX = 0f;
320
321 /**
Joshua Tsuji14e68552019-06-06 17:17:08 -0400322 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
323 */
324 private Runnable mAnimateInFlyout;
325
326 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -0400327 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
328 * it immediately.
329 */
330 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
331 (dynamicAnimation, b, v, v1) -> {
332 if (mFlyoutDragDeltaX == 0) {
333 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
334 } else {
335 mFlyout.hideFlyout();
336 }
337 };
338
Lyn Han1b4f25e2019-06-11 13:56:34 -0700339 @NonNull
340 private final SurfaceSynchronizer mSurfaceSynchronizer;
Issei Suzukic0387542019-03-08 17:31:14 +0100341
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400342 private final NotificationShadeWindowController mNotificationShadeWindowController;
343
Joshua Tsuji20103542020-02-18 14:06:28 -0500344 /**
345 * The currently magnetized object, which is being dragged and will be attracted to the magnetic
346 * dismiss target.
347 *
348 * This is either the stack itself, or an individual bubble.
349 */
350 private MagnetizedObject<?> mMagnetizedObject;
351
352 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500353 * The MagneticTarget instance for our circular dismiss view. This is added to the
354 * MagnetizedObject instances for the stack and any dragged-out bubbles.
355 */
356 private MagnetizedObject.MagneticTarget mMagneticTarget;
357
358 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
359 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
360 new MagnetizedObject.MagnetListener() {
361 @Override
362 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
Joshua Tsuji7db91c32020-05-20 22:10:10 -0400363 if (mExpandedAnimationController.getDraggedOutBubble() == null) {
364 return;
365 }
366
Joshua Tsuji20103542020-02-18 14:06:28 -0500367 animateDesaturateAndDarken(
368 mExpandedAnimationController.getDraggedOutBubble(), true);
369 }
370
371 @Override
372 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
373 float velX, float velY, boolean wasFlungOut) {
Joshua Tsuji7db91c32020-05-20 22:10:10 -0400374 if (mExpandedAnimationController.getDraggedOutBubble() == null) {
375 return;
376 }
377
Joshua Tsuji20103542020-02-18 14:06:28 -0500378 animateDesaturateAndDarken(
379 mExpandedAnimationController.getDraggedOutBubble(), false);
380
381 if (wasFlungOut) {
382 mExpandedAnimationController.snapBubbleBack(
383 mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
384 hideDismissTarget();
385 } else {
386 mExpandedAnimationController.onUnstuckFromTarget();
387 }
388 }
389
390 @Override
391 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
Joshua Tsuji7db91c32020-05-20 22:10:10 -0400392 if (mExpandedAnimationController.getDraggedOutBubble() == null) {
393 return;
394 }
395
Joshua Tsuji20103542020-02-18 14:06:28 -0500396 mExpandedAnimationController.dismissDraggedOutBubble(
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400397 mExpandedAnimationController.getDraggedOutBubble() /* bubble */,
398 mDismissTargetContainer.getHeight() /* translationYBy */,
399 BubbleStackView.this::dismissMagnetizedObject /* after */);
Joshua Tsuji20103542020-02-18 14:06:28 -0500400 hideDismissTarget();
401 }
402 };
403
404 /** Magnet listener that handles animating and dismissing the entire stack. */
405 private final MagnetizedObject.MagnetListener mStackMagnetListener =
406 new MagnetizedObject.MagnetListener() {
407 @Override
408 public void onStuckToTarget(
409 @NonNull MagnetizedObject.MagneticTarget target) {
410 animateDesaturateAndDarken(mBubbleContainer, true);
411 }
412
413 @Override
414 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
415 float velX, float velY, boolean wasFlungOut) {
416 animateDesaturateAndDarken(mBubbleContainer, false);
417
418 if (wasFlungOut) {
419 mStackAnimationController.flingStackThenSpringToEdge(
420 mStackAnimationController.getStackPosition().x, velX, velY);
421 hideDismissTarget();
422 } else {
423 mStackAnimationController.onUnstuckFromTarget();
424 }
425 }
426
427 @Override
428 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400429 mStackAnimationController.animateStackDismissal(
430 mDismissTargetContainer.getHeight() /* translationYBy */,
Joshua Tsuji20103542020-02-18 14:06:28 -0500431 () -> {
432 resetDesaturationAndDarken();
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400433 dismissMagnetizedObject();
Joshua Tsuji20103542020-02-18 14:06:28 -0500434 }
435 );
436
437 hideDismissTarget();
438 }
439 };
440
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400441 /**
442 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack.
443 * When expanded, clicking a bubble either expands that bubble, or collapses the stack.
444 */
445 private OnClickListener mBubbleClickListener = new OnClickListener() {
446 @Override
447 public void onClick(View view) {
448 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view);
449
450 // If the bubble has since left us, ignore the click.
451 if (clickedBubble == null) {
452 return;
453 }
454
455 final boolean clickedBubbleIsCurrentlyExpandedBubble =
456 clickedBubble.getKey().equals(mExpandedBubble.getKey());
457
Joshua Tsuji5578b7c2020-05-12 18:32:14 -0400458 if (isExpanded()) {
459 mExpandedAnimationController.onGestureFinished();
460 }
461
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400462 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) {
463 if (clickedBubble != mBubbleData.getSelectedBubble()) {
464 // Select the clicked bubble.
465 mBubbleData.setSelectedBubble(clickedBubble);
466 } else {
467 // If the clicked bubble is the selected bubble (but not the expanded bubble),
468 // that means overflow was previously expanded. Set the selected bubble
469 // internally without going through BubbleData (which would ignore it since it's
470 // already selected).
471 setSelectedBubble(clickedBubble);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400472 }
473 } else {
474 // Otherwise, we either tapped the stack (which means we're collapsed
475 // and should expand) or the currently selected bubble (we're expanded
476 // and should collapse).
477 if (!maybeShowStackUserEducation()) {
478 mBubbleData.setExpanded(!mBubbleData.isExpanded());
479 }
480 }
481 }
482 };
483
484 /**
485 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when
486 * collapsed), or individual bubbles (when expanded).
487 */
488 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() {
489
490 @Override
491 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
492 // If we're expanding or collapsing, consume but ignore all touch events.
493 if (mIsExpansionAnimating) {
494 return true;
495 }
496
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400497 // If the manage menu is visible, just hide it.
498 if (mShowingManage) {
499 showManageMenu(false /* show */);
500 }
501
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400502 if (mBubbleData.isExpanded()) {
503 maybeShowManageEducation(false /* show */);
504
505 // If we're expanded, tell the animation controller to prepare to drag this bubble,
506 // dispatching to the individual bubble magnet listener.
507 mExpandedAnimationController.prepareForBubbleDrag(
508 v /* bubble */,
509 mMagneticTarget,
510 mIndividualBubbleMagnetListener);
511
512 // Save the magnetized individual bubble so we can dispatch touch events to it.
513 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
514 } else {
515 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the
516 // animation controller, and hide the flyout.
517 mStackAnimationController.cancelStackPositionAnimations();
518 mBubbleContainer.setActiveController(mStackAnimationController);
519 hideFlyoutImmediate();
520
521 // Also, save the magnetized stack so we can dispatch touch events to it.
522 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
523 mMagnetizedObject.setMagnetListener(mStackMagnetListener);
524 }
525
526 passEventToMagnetizedObject(ev);
527
528 // Bubbles are always interested in all touch events!
529 return true;
530 }
531
532 @Override
533 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
534 float viewInitialY, float dx, float dy) {
535 // If we're expanding or collapsing, ignore all touch events.
536 if (mIsExpansionAnimating) {
537 return;
538 }
539
540 // Show the dismiss target, if we haven't already.
541 springInDismissTargetMaybe();
542
543 // First, see if the magnetized object consumes the event - if so, we shouldn't move the
544 // bubble since it's stuck to the target.
545 if (!passEventToMagnetizedObject(ev)) {
546 if (mBubbleData.isExpanded()) {
547 mExpandedAnimationController.dragBubbleOut(
548 v, viewInitialX + dx, viewInitialY + dy);
549 } else {
550 hideStackUserEducation(false /* fromExpansion */);
551 mStackAnimationController.moveStackFromTouch(
552 viewInitialX + dx, viewInitialY + dy);
553 }
554 }
555 }
556
557 @Override
558 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
559 float viewInitialY, float dx, float dy, float velX, float velY) {
560 // If we're expanding or collapsing, ignore all touch events.
561 if (mIsExpansionAnimating) {
562 return;
563 }
564
565 // First, see if the magnetized object consumes the event - if so, the bubble was
566 // released in the target or flung out of it, and we should ignore the event.
567 if (!passEventToMagnetizedObject(ev)) {
568 if (mBubbleData.isExpanded()) {
569 mExpandedAnimationController.snapBubbleBack(v, velX, velY);
570 } else {
571 // Fling the stack to the edge, and save whether or not it's going to end up on
572 // the left side of the screen.
573 mStackOnLeftOrWillBe =
574 mStackAnimationController.flingStackThenSpringToEdge(
575 viewInitialX + dx, velX, velY) <= 0;
576
577 updateBubbleZOrdersAndDotPosition(true /* animate */);
578
579 logBubbleEvent(null /* no bubble associated with bubble stack move */,
580 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
581 }
582
583 hideDismissTarget();
584 }
585 }
586 };
587
588 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
589 private OnClickListener mFlyoutClickListener = new OnClickListener() {
590 @Override
591 public void onClick(View view) {
592 if (maybeShowStackUserEducation()) {
593 // If we're showing user education, don't open the bubble show the education first
594 mBubbleToExpandAfterFlyoutCollapse = null;
595 } else {
596 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
597 }
598
599 mFlyout.removeCallbacks(mHideFlyout);
600 mHideFlyout.run();
601 }
602 };
603
604 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */
605 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() {
606
607 @Override
608 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
609 mFlyout.removeCallbacks(mHideFlyout);
610 return true;
611 }
612
613 @Override
614 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
615 float viewInitialY, float dx, float dy) {
616 setFlyoutStateForDragLength(dx);
617 }
618
619 @Override
620 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
621 float viewInitialY, float dx, float dy, float velX, float velY) {
622 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
623 final boolean metRequiredVelocity =
624 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
625 final boolean metRequiredDeltaX =
626 onLeft
627 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
628 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
629 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
630 final boolean shouldDismiss = metRequiredVelocity
631 || (metRequiredDeltaX && !isCancelFling);
632
633 mFlyout.removeCallbacks(mHideFlyout);
634 animateFlyoutCollapsed(shouldDismiss, velX);
635
636 maybeShowStackUserEducation();
637 }
638 };
639
Joshua Tsuji20103542020-02-18 14:06:28 -0500640 private ViewGroup mDismissTargetContainer;
641 private PhysicsAnimator<View> mDismissTargetAnimator;
642 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
643 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Issei Suzukic0387542019-03-08 17:31:14 +0100644
Lyn Hanf4730312019-06-18 11:18:58 -0700645 private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
Lyn Han3cd75d72020-02-15 19:10:12 -0800646
Joshua Tsujia2433db2020-03-12 17:56:22 -0400647 @Nullable
Lyn Han3cd75d72020-02-15 19:10:12 -0800648 private BubbleOverflow mBubbleOverflow;
Lyn Hanf4730312019-06-18 11:18:58 -0700649
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800650 private boolean mShouldShowUserEducation;
651 private boolean mAnimatingEducationAway;
652 private View mUserEducationView;
653
654 private boolean mShouldShowManageEducation;
655 private BubbleManageEducationView mManageEducationView;
656 private boolean mAnimatingManageEducationAway;
657
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400658 private ViewGroup mManageMenu;
659 private ImageView mManageSettingsIcon;
660 private TextView mManageSettingsText;
661 private boolean mShowingManage = false;
662 private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig(
663 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400664 @SuppressLint("ClickableViewAccessibility")
Issei Suzukic0387542019-03-08 17:31:14 +0100665 public BubbleStackView(Context context, BubbleData data,
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500666 @Nullable SurfaceSynchronizer synchronizer,
Joshua Tsujibe60a582020-03-23 17:17:26 -0400667 FloatingContentCoordinator floatingContentCoordinator,
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400668 SysUiState sysUiState,
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400669 NotificationShadeWindowController notificationShadeWindowController,
670 Runnable allBubblesAnimatedOutAction) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800671 super(context);
672
Mady Mellorcfd06c12019-02-13 14:32:12 -0800673 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800674 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800675
Joshua Tsujibe60a582020-03-23 17:17:26 -0400676 mSysUiState = sysUiState;
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400677 mNotificationShadeWindowController = notificationShadeWindowController;
Joshua Tsujibe60a582020-03-23 17:17:26 -0400678
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800679 Resources res = getResources();
Mady Mellor70958542019-09-24 17:12:46 -0700680 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800681 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellor70958542019-09-24 17:12:46 -0700682 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Lyn Han4a8efe32019-05-30 09:43:27 -0700683 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellore9371bc2019-07-10 18:50:59 -0700684 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800685 mExpandedAnimateXDistance =
686 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
687 mExpandedAnimateYDistance =
688 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700689 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
690
Joshua Tsujif44347f2019-02-12 14:28:06 -0500691 mStatusBarHeight =
692 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500693 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800694
695 mDisplaySize = new Point();
696 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700697 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700698 wm.getDefaultDisplay().getRealSize(mDisplaySize);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800699
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700700 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800701 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800702
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400703 final Runnable onBubbleAnimatedOut = () -> {
704 if (getBubbleCount() == 0) {
705 allBubblesAnimatedOutAction.run();
706 }
707 };
708
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400709 mStackAnimationController = new StackAnimationController(
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400710 floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut);
Lyn Hanf4730312019-06-18 11:18:58 -0700711
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700712 mExpandedAnimationController = new ExpandedAnimationController(
Joshua Tsuji4395bbd2020-05-19 17:53:33 -0400713 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation,
714 onBubbleAnimatedOut);
Issei Suzukic0387542019-03-08 17:31:14 +0100715 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800716
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800717 setUpUserEducation();
718
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800719 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400720 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800721 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800722 mBubbleContainer.setClipChildren(false);
723 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
724
Mady Mellor3dff9e62019-02-05 18:12:53 -0800725 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800726 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700727 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
728 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800729 mExpandedViewContainer.setClipChildren(false);
730 addView(mExpandedViewContainer);
731
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400732 setUpManageMenu();
733
Mady Mellor8bfe5412019-07-31 14:56:44 -0700734 setUpFlyout();
Joshua Tsuji6549e702019-05-02 13:13:16 -0400735 mFlyoutTransitionSpring.setSpring(new SpringForce()
Joshua Tsuji14e68552019-06-06 17:17:08 -0400736 .setStiffness(SpringForce.STIFFNESS_LOW)
Joshua Tsuji6549e702019-05-02 13:13:16 -0400737 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
738 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
739
Joshua Tsujie48c4112020-02-26 14:36:25 -0500740 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
Joshua Tsuji20103542020-02-18 14:06:28 -0500741 final View targetView = new DismissCircleView(context);
742 final FrameLayout.LayoutParams newParams =
Joshua Tsujie48c4112020-02-26 14:36:25 -0500743 new FrameLayout.LayoutParams(targetSize, targetSize);
Joshua Tsuji20103542020-02-18 14:06:28 -0500744 newParams.gravity = Gravity.CENTER;
745 targetView.setLayoutParams(newParams);
746 mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
747
748 mDismissTargetContainer = new FrameLayout(context);
749 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
Joshua Tsuji6549e702019-05-02 13:13:16 -0400750 MATCH_PARENT,
Joshua Tsujif39539d2020-04-03 18:53:06 -0400751 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
Joshua Tsuji6549e702019-05-02 13:13:16 -0400752 Gravity.BOTTOM));
Joshua Tsuji20103542020-02-18 14:06:28 -0500753 mDismissTargetContainer.setClipChildren(false);
754 mDismissTargetContainer.addView(targetView);
755 mDismissTargetContainer.setVisibility(View.INVISIBLE);
756 addView(mDismissTargetContainer);
757
758 // Start translated down so the target springs up.
759 targetView.setTranslationY(
Joshua Tsujif39539d2020-04-03 18:53:06 -0400760 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height));
Joshua Tsuji20103542020-02-18 14:06:28 -0500761
Joshua Tsuji4f8eea22020-05-21 00:16:06 -0400762 final ContentResolver contentResolver = getContext().getContentResolver();
763 final int dismissRadius = Settings.Secure.getInt(
764 contentResolver, "bubble_dismiss_radius", mBubbleSize * 2 /* default */);
765
Joshua Tsuji20103542020-02-18 14:06:28 -0500766 // Save the MagneticTarget instance for the newly set up view - we'll add this to the
767 // MagnetizedObjects.
Joshua Tsuji4f8eea22020-05-21 00:16:06 -0400768 mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, dismissRadius);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400769
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800770 mExpandedViewXAnim =
771 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
772 mExpandedViewXAnim.setSpring(
773 new SpringForce()
774 .setStiffness(SpringForce.STIFFNESS_LOW)
775 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
776
777 mExpandedViewYAnim =
778 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
779 mExpandedViewYAnim.setSpring(
780 new SpringForce()
781 .setStiffness(SpringForce.STIFFNESS_LOW)
782 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorbc078c22019-03-26 17:10:34 -0700783 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700784 if (mIsExpanded && mExpandedBubble != null
785 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -0800786 mExpandedBubble.getExpandedView().updateView();
Mady Mellorbc078c22019-03-26 17:10:34 -0700787 }
788 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800789
790 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800791 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500792 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800793
Lyn Hancd4f87e2020-02-19 20:33:45 -0800794 setUpOverflow();
Lyn Hanb58c7562020-01-07 14:29:20 -0800795
Mady Mellor5d8f1402019-02-21 18:23:52 -0800796 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
Mady Mellorbc078c22019-03-26 17:10:34 -0700797 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800798 return view.onApplyWindowInsets(insets);
799 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800800 mExpandedAnimationController.updateYPosition(
801 // Update the insets after we're done translating otherwise position
802 // calculation for them won't be correct.
Lyn Hanb58c7562020-01-07 14:29:20 -0800803 () -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700804 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800805 mExpandedBubble.getExpandedView().updateInsets(insets);
806 }
807 });
Mady Mellor5d8f1402019-02-21 18:23:52 -0800808 return view.onApplyWindowInsets(insets);
809 });
Mark Renouf821e6782019-04-01 14:17:37 -0400810
Lyn Hanf4730312019-06-18 11:18:58 -0700811 mOrientationChangedListener =
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400812 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
Lyn Hanb4b06132020-05-11 09:25:20 -0700813 mExpandedAnimationController.updateResources(mOrientation, mDisplaySize);
814 mStackAnimationController.updateResources(mOrientation);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700815
816 // Reposition & adjust the height for new orientation
817 if (mIsExpanded) {
818 mExpandedViewContainer.setTranslationY(getExpandedViewY());
Mady Melloradd5c6a92020-03-31 17:22:48 -0700819 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800820 mExpandedBubble.getExpandedView().updateView();
821 }
Mady Mellor9be3bed2019-08-21 17:26:26 -0700822 }
823
824 // Need to update the padding around the view
825 WindowInsets insets = getRootWindowInsets();
826 int leftPadding = mExpandedViewPadding;
827 int rightPadding = mExpandedViewPadding;
828 if (insets != null) {
829 // Can't have the expanded view overlaying notches
830 int cutoutLeft = 0;
831 int cutoutRight = 0;
832 DisplayCutout cutout = insets.getDisplayCutout();
833 if (cutout != null) {
834 cutoutLeft = cutout.getSafeInsetLeft();
835 cutoutRight = cutout.getSafeInsetRight();
836 }
837 // Or overlaying nav or status bar
838 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
839 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
840 }
841 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
842 rightPadding, mExpandedViewPadding);
843
Lyn Hanf4730312019-06-18 11:18:58 -0700844 if (mIsExpanded) {
845 // Re-draw bubble row and pointer for new orientation.
846 mExpandedAnimationController.expandFromStack(() -> {
847 updatePointerPosition();
848 } /* after */);
849 }
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400850 if (mVerticalPosPercentBeforeRotation >= 0) {
851 mStackAnimationController.moveStackToSimilarPositionAfterRotation(
852 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
853 }
Lyn Hanf4730312019-06-18 11:18:58 -0700854 removeOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400855 };
856
Mark Renouf821e6782019-04-01 14:17:37 -0400857 // This must be a separate OnDrawListener since it should be called for every draw.
858 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400859
860 final ColorMatrix animatedMatrix = new ColorMatrix();
861 final ColorMatrix darkenMatrix = new ColorMatrix();
862
863 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
864 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
865 final float animatedValue = (float) animation.getAnimatedValue();
866 animatedMatrix.setSaturation(animatedValue);
867
868 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
869 darkenMatrix.setScale(
870 1f - animatedDarkenValue /* red */,
871 1f - animatedDarkenValue /* green */,
872 1f - animatedDarkenValue /* blue */,
873 1f /* alpha */);
874
875 // Concat the matrices so that the animatedMatrix both desaturates and darkens.
876 animatedMatrix.postConcat(darkenMatrix);
877
878 // Update the paint and apply it to the bubble container.
879 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
880 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
881 });
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400882
883 // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts,
884 // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
885 setOnTouchListener((view, ev) -> {
886 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400887 if (mShowingManage) {
888 showManageMenu(false /* show */);
889 } else if (mBubbleData.isExpanded()) {
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400890 mBubbleData.setExpanded(false);
891 }
892 }
893
Joshua Tsuji04ab6402020-05-20 14:54:34 -0400894 return true;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400895 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800896 }
897
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400898 private void setUpManageMenu() {
899 if (mManageMenu != null) {
900 removeView(mManageMenu);
901 }
902
903 mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate(
904 R.layout.bubble_manage_menu, this, false);
905 mManageMenu.setVisibility(View.INVISIBLE);
906
907 PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig);
908
909 final TypedArray ta = mContext.obtainStyledAttributes(
910 new int[] {android.R.attr.dialogCornerRadius});
911 final int menuCornerRadius = ta.getDimensionPixelSize(0, 0);
912 ta.recycle();
913
914 mManageMenu.setOutlineProvider(new ViewOutlineProvider() {
915 @Override
916 public void getOutline(View view, Outline outline) {
917 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), menuCornerRadius);
918 }
919 });
920 mManageMenu.setClipToOutline(true);
921
922 mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener(
923 view -> {
924 showManageMenu(false /* show */);
925 dismissBubbleIfExists(mBubbleData.getSelectedBubble());
926 });
927
928 mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener(
929 view -> {
930 showManageMenu(false /* show */);
931 final Bubble bubble = mBubbleData.getSelectedBubble();
Lyn Han2f6e89d2020-04-15 10:01:01 -0700932 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400933 mUnbubbleConversationCallback.accept(bubble.getEntry());
934 }
935 });
936
937 mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener(
938 view -> {
939 showManageMenu(false /* show */);
940 final Bubble bubble = mBubbleData.getSelectedBubble();
Lyn Han2f6e89d2020-04-15 10:01:01 -0700941 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
Pinyao Ting3c930612020-05-19 00:26:03 +0000942 final Intent intent = bubble.getSettingsIntent(mContext);
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400943 collapseStack(() -> {
Pinyao Ting3c930612020-05-19 00:26:03 +0000944
945 mContext.startActivityAsUser(intent, bubble.getUser());
Joshua Tsuji6855cab2020-04-16 01:05:39 -0400946 logBubbleClickEvent(
947 bubble,
948 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
949 });
950 }
951 });
952
953 mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon);
954 mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name);
955 addView(mManageMenu);
956 }
957
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800958 private void setUpUserEducation() {
959 if (mUserEducationView != null) {
960 removeView(mUserEducationView);
961 }
962 mShouldShowUserEducation = shouldShowBubblesEducation();
963 if (DEBUG_USER_EDUCATION) {
964 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
965 }
966 if (mShouldShowUserEducation) {
967 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
968 false /* attachToRoot */);
969 mUserEducationView.setVisibility(GONE);
970
971 final TypedArray ta = mContext.obtainStyledAttributes(
972 new int[] {android.R.attr.colorAccent,
973 android.R.attr.textColorPrimaryInverse});
974 final int bgColor = ta.getColor(0, Color.BLACK);
975 int textColor = ta.getColor(1, Color.WHITE);
976 ta.recycle();
977 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
978
979 TextView title = mUserEducationView.findViewById(R.id.user_education_title);
980 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
981 title.setTextColor(textColor);
982 description.setTextColor(textColor);
983
984 addView(mUserEducationView);
985 }
986
987 if (mManageEducationView != null) {
988 removeView(mManageEducationView);
989 }
990 mShouldShowManageEducation = shouldShowManageEducation();
991 if (DEBUG_USER_EDUCATION) {
992 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
993 }
994 if (mShouldShowManageEducation) {
995 mManageEducationView = (BubbleManageEducationView)
996 mInflater.inflate(R.layout.bubbles_manage_button_education, this,
997 false /* attachToRoot */);
998 mManageEducationView.setVisibility(GONE);
999 mManageEducationView.setElevation(mBubbleElevation);
1000
1001 addView(mManageEducationView);
Lyn Hanb58c7562020-01-07 14:29:20 -08001002 }
Lyn Hanb58c7562020-01-07 14:29:20 -08001003 }
1004
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001005 @SuppressLint("ClickableViewAccessibility")
Mady Mellor8bfe5412019-07-31 14:56:44 -07001006 private void setUpFlyout() {
1007 if (mFlyout != null) {
1008 removeView(mFlyout);
1009 }
1010 mFlyout = new BubbleFlyoutView(getContext());
1011 mFlyout.setVisibility(GONE);
1012 mFlyout.animate()
1013 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
1014 .setInterpolator(new AccelerateDecelerateInterpolator());
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001015 mFlyout.setOnClickListener(mFlyoutClickListener);
1016 mFlyout.setOnTouchListener(mFlyoutTouchListener);
Mady Mellor8bfe5412019-07-31 14:56:44 -07001017 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
1018 }
1019
Lyn Hancd4f87e2020-02-19 20:33:45 -08001020 private void setUpOverflow() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001021 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1022 return;
1023 }
Lyn Hancd4f87e2020-02-19 20:33:45 -08001024 int overflowBtnIndex = 0;
1025 if (mBubbleOverflow == null) {
Mady Mellor0122cc92020-02-27 12:15:39 -08001026 mBubbleOverflow = new BubbleOverflow(getContext());
1027 mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
Lyn Hancd4f87e2020-02-19 20:33:45 -08001028 } else {
1029 mBubbleContainer.removeView(mBubbleOverflow.getBtn());
Lyn Hanbc985c52020-05-18 17:57:58 -07001030 mBubbleOverflow.updateDimensions();
Lyn Hanb4b06132020-05-11 09:25:20 -07001031 mBubbleOverflow.updateIcon(mContext,this);
Lyn Hane1cf3b22020-04-01 13:39:43 -07001032 overflowBtnIndex = mBubbleContainer.getChildCount();
Lyn Hancd4f87e2020-02-19 20:33:45 -08001033 }
1034 mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
1035 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
1036
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001037 mBubbleOverflow.getBtn().setOnClickListener(v -> setSelectedBubble(mBubbleOverflow));
Lyn Hancd4f87e2020-02-19 20:33:45 -08001038 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -07001039 /**
Lyn Han02cca812019-04-02 16:27:32 -07001040 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -07001041 */
Lyn Han02cca812019-04-02 16:27:32 -07001042 public void onThemeChanged() {
Mady Mellor8bfe5412019-07-31 14:56:44 -07001043 setUpFlyout();
Lyn Hancd4f87e2020-02-19 20:33:45 -08001044 setUpOverflow();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001045 setUpUserEducation();
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001046 setUpManageMenu();
Lyn Hanf1c9b8b2019-03-14 16:49:48 -07001047 }
1048
Joshua Tsujif418f9e2019-04-04 17:09:53 -04001049 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
Lyn Hanf4730312019-06-18 11:18:58 -07001050 public void onOrientationChanged(int orientation) {
1051 mOrientation = orientation;
1052
Mady Mellore19353d2019-08-21 17:25:02 -07001053 // Display size is based on the rotation device was in when requested, we should update it
Mady Mellor9be3bed2019-08-21 17:26:26 -07001054 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -07001055 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
1056 wm.getDefaultDisplay().getRealSize(mDisplaySize);
1057
Mady Mellor818eef02019-08-16 16:12:29 -07001058 // Some resources change depending on orientation
1059 Resources res = getContext().getResources();
1060 mStatusBarHeight = res.getDimensionPixelSize(
1061 com.android.internal.R.dimen.status_bar_height);
1062 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
1063
Joshua Tsujif418f9e2019-04-04 17:09:53 -04001064 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
1065 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
1066 mVerticalPosPercentBeforeRotation =
1067 (mStackAnimationController.getStackPosition().y - allowablePos.top)
1068 / (allowablePos.bottom - allowablePos.top);
Joshua Tsujifaf890f2020-04-16 13:38:32 -04001069 mVerticalPosPercentBeforeRotation =
1070 Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation));
Lyn Hanf4730312019-06-18 11:18:58 -07001071 addOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -04001072 hideFlyoutImmediate();
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001073
1074 mManageMenu.setVisibility(View.INVISIBLE);
1075 mShowingManage = false;
Joshua Tsujif418f9e2019-04-04 17:09:53 -04001076 }
1077
Lyn Hanb4b06132020-05-11 09:25:20 -07001078 /** Respond to the display size change by recalculating view size and location. */
1079 public void onDisplaySizeChanged() {
1080 setUpOverflow();
1081
1082 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
1083 wm.getDefaultDisplay().getRealSize(mDisplaySize);
1084 Resources res = getContext().getResources();
1085 mStatusBarHeight = res.getDimensionPixelSize(
1086 com.android.internal.R.dimen.status_bar_height);
1087 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
1088 mBubbleSize = getResources().getDimensionPixelSize(R.dimen.individual_bubble_size);
1089 for (Bubble b : mBubbleData.getBubbles()) {
1090 if (b.getIconView() == null) {
1091 Log.d(TAG, "Display size changed. Icon null: " + b);
1092 continue;
1093 }
1094 b.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
1095 }
1096 mExpandedAnimationController.updateResources(mOrientation, mDisplaySize);
1097 mStackAnimationController.updateResources(mOrientation);
1098 }
1099
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001100 @Override
Joshua Tsujid9923e52020-04-29 15:33:01 -04001101 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
1102 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
1103
Joshua Tsuji5a55f0852020-05-11 15:46:30 -04001104 mTempRect.setEmpty();
Joshua Tsujid9923e52020-04-29 15:33:01 -04001105 getTouchableRegion(mTempRect);
1106 inoutInfo.touchableRegion.set(mTempRect);
1107 }
1108
1109 @Override
1110 protected void onAttachedToWindow() {
1111 super.onAttachedToWindow();
1112 getViewTreeObserver().addOnComputeInternalInsetsListener(this);
Mady Mellor217b2e92019-02-27 11:44:16 -08001113 }
1114
1115 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001116 protected void onDetachedFromWindow() {
1117 super.onDetachedFromWindow();
1118 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Joshua Tsujid9923e52020-04-29 15:33:01 -04001119 getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001120 }
1121
1122 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -08001123 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
1124 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Han670fa802020-05-05 12:56:33 -07001125 setupLocalMenu(info);
1126 }
Lyn Hane68d0912019-05-02 18:28:01 -07001127
Lyn Han670fa802020-05-05 12:56:33 -07001128 void setupLocalMenu(AccessibilityNodeInfo info) {
1129 Resources res = mContext.getResources();
1130
1131 // Custom local actions.
Lyn Hane68d0912019-05-02 18:28:01 -07001132 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
Lyn Han670fa802020-05-05 12:56:33 -07001133 res.getString(R.string.bubble_accessibility_action_move_top_left));
Lyn Hane68d0912019-05-02 18:28:01 -07001134 info.addAction(moveTopLeft);
1135
1136 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
Lyn Han670fa802020-05-05 12:56:33 -07001137 res.getString(R.string.bubble_accessibility_action_move_top_right));
Lyn Hane68d0912019-05-02 18:28:01 -07001138 info.addAction(moveTopRight);
1139
1140 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
Lyn Han670fa802020-05-05 12:56:33 -07001141 res.getString(R.string.bubble_accessibility_action_move_bottom_left));
Lyn Hane68d0912019-05-02 18:28:01 -07001142 info.addAction(moveBottomLeft);
1143
1144 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
Lyn Han670fa802020-05-05 12:56:33 -07001145 res.getString(R.string.bubble_accessibility_action_move_bottom_right));
Lyn Hane68d0912019-05-02 18:28:01 -07001146 info.addAction(moveBottomRight);
1147
1148 // Default actions.
1149 info.addAction(AccessibilityAction.ACTION_DISMISS);
Mady Mellor217b2e92019-02-27 11:44:16 -08001150 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -07001151 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -08001152 } else {
Lyn Hane68d0912019-05-02 18:28:01 -07001153 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -08001154 }
1155 }
1156
1157 @Override
1158 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1159 if (super.performAccessibilityActionInternal(action, arguments)) {
1160 return true;
1161 }
Lyn Hane68d0912019-05-02 18:28:01 -07001162 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
1163
1164 // R constants are not final so we cannot use switch-case here.
1165 if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
1166 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
Joshua Tsuji0f390fb2020-04-28 15:20:10 -04001167 announceForAccessibility(
1168 getResources().getString(R.string.accessibility_bubble_dismissed));
Lyn Hane68d0912019-05-02 18:28:01 -07001169 return true;
1170 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
1171 mBubbleData.setExpanded(false);
1172 return true;
1173 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
1174 mBubbleData.setExpanded(true);
1175 return true;
1176 } else if (action == R.id.action_move_top_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001177 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001178 return true;
1179 } else if (action == R.id.action_move_top_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001180 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001181 return true;
1182 } else if (action == R.id.action_move_bottom_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001183 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001184 return true;
1185 } else if (action == R.id.action_move_bottom_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001186 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001187 return true;
Mady Mellor217b2e92019-02-27 11:44:16 -08001188 }
1189 return false;
1190 }
1191
Lyn Han6c40fe72019-05-08 14:06:33 -07001192 /**
1193 * Update content description for a11y TalkBack.
1194 */
1195 public void updateContentDescription() {
1196 if (mBubbleData.getBubbles().isEmpty()) {
1197 return;
1198 }
Lyn Han6c40fe72019-05-08 14:06:33 -07001199
Joshua Tsuji0f390fb2020-04-28 15:20:10 -04001200 for (int i = 0; i < mBubbleData.getBubbles().size(); i++) {
1201 final Bubble bubble = mBubbleData.getBubbles().get(i);
1202 final String appName = bubble.getAppName();
Lyn Han6c40fe72019-05-08 14:06:33 -07001203
Pinyao Ting3c930612020-05-19 00:26:03 +00001204 String titleStr = bubble.getTitle();
1205 if (titleStr == null) {
1206 titleStr = getResources().getString(R.string.notification_bubble_title);
Joshua Tsuji0f390fb2020-04-28 15:20:10 -04001207 }
Lyn Han6c40fe72019-05-08 14:06:33 -07001208
Joshua Tsuji0f390fb2020-04-28 15:20:10 -04001209 if (bubble.getIconView() != null) {
1210 if (mIsExpanded || i > 0) {
1211 bubble.getIconView().setContentDescription(getResources().getString(
1212 R.string.bubble_content_description_single, titleStr, appName));
1213 } else {
1214 final int moreCount = mBubbleContainer.getChildCount() - 1;
1215 bubble.getIconView().setContentDescription(getResources().getString(
1216 R.string.bubble_content_description_stack,
1217 titleStr, appName, moreCount));
1218 }
Lyn Han6c40fe72019-05-08 14:06:33 -07001219 }
1220 }
1221 }
1222
Mark Renouf821e6782019-04-01 14:17:37 -04001223 private void updateSystemGestureExcludeRects() {
1224 // Exclude the region occupied by the first BubbleView in the stack
1225 Rect excludeZone = mSystemGestureExclusionRects.get(0);
Lyn Hanc47e1712020-01-28 21:43:34 -08001226 if (getBubbleCount() > 0) {
Mark Renouf821e6782019-04-01 14:17:37 -04001227 View firstBubble = mBubbleContainer.getChildAt(0);
1228 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
1229 firstBubble.getBottom());
1230 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
1231 (int) (firstBubble.getTranslationY() + 0.5f));
1232 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
1233 } else {
1234 excludeZone.setEmpty();
1235 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
1236 }
1237 }
1238
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001239 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -08001240 * Sets the listener to notify when the bubble stack is expanded.
1241 */
1242 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
1243 mExpandListener = listener;
1244 }
1245
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001246 /** Sets the function to call to un-bubble the given conversation. */
1247 public void setUnbubbleConversationCallback(
1248 Consumer<NotificationEntry> unbubbleConversationCallback) {
1249 mUnbubbleConversationCallback = unbubbleConversationCallback;
1250 }
1251
Mady Mellorcd9b1302018-11-06 18:08:04 -08001252 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001253 * Whether the stack of bubbles is expanded or not.
1254 */
1255 public boolean isExpanded() {
1256 return mIsExpanded;
1257 }
1258
1259 /**
Mady Mellor047e24e2019-08-05 11:35:40 -07001260 * Whether the stack of bubbles is animating to or from expansion.
1261 */
1262 public boolean isExpansionAnimating() {
1263 return mIsExpansionAnimating;
1264 }
1265
1266 /**
Mady Mellorb8aaf972019-11-26 10:28:00 -08001267 * The {@link BadgedImageView} that is expanded, null if one does not exist.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001268 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001269 View getExpandedBubbleView() {
Mady Mellored99c272019-06-13 15:58:30 -07001270 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
Mady Mellor3dff9e62019-02-05 18:12:53 -08001271 }
1272
1273 /**
1274 * The {@link Bubble} that is expanded, null if one does not exist.
1275 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001276 @Nullable
Lyn Han9f66c3b2020-03-05 23:59:29 -08001277 BubbleViewProvider getExpandedBubble() {
1278 return mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001279 }
1280
Mark Renouf71a3af62019-04-08 15:02:54 -04001281 // via BubbleData.Listener
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001282 @SuppressLint("ClickableViewAccessibility")
Mark Renouf71a3af62019-04-08 15:02:54 -04001283 void addBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001284 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001285 Log.d(TAG, "addBubble: " + bubble);
1286 }
Joshua Tsujib35f5912019-07-24 16:15:21 -04001287
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001288 if (getBubbleCount() == 0 && mShouldShowUserEducation) {
1289 // Override the default stack position if we're showing user education.
1290 mStackAnimationController.setStackPosition(
1291 mStackAnimationController.getDefaultStartPosition());
1292 }
1293
Lyn Hanc47e1712020-01-28 21:43:34 -08001294 if (getBubbleCount() == 0) {
Joshua Tsujib35f5912019-07-24 16:15:21 -04001295 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
1296 }
1297
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001298 if (bubble.getIconView() == null) {
1299 return;
1300 }
1301
Joshua Tsujib35f5912019-07-24 16:15:21 -04001302 // Set the dot position to the opposite of the side the stack is resting on, since the stack
1303 // resting slightly off-screen would result in the dot also being off-screen.
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001304 bubble.getIconView().setDotPositionOnLeft(
Joshua Tsujib35f5912019-07-24 16:15:21 -04001305 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
1306
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001307 bubble.getIconView().setOnClickListener(mBubbleClickListener);
1308 bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
1309
Mady Mellored99c272019-06-13 15:58:30 -07001310 mBubbleContainer.addView(bubble.getIconView(), 0,
Mark Renouf71a3af62019-04-08 15:02:54 -04001311 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellored99c272019-06-13 15:58:30 -07001312 ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
Mark Renoufba5ab512019-05-02 15:21:01 -04001313 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001314 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001315 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001316 }
1317
1318 // via BubbleData.Listener
1319 void removeBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001320 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001321 Log.d(TAG, "removeBubble: " + bubble);
1322 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001323 // Remove it from the views
Lyn Hane1395572020-03-23 15:48:54 -07001324 for (int i = 0; i < getBubbleCount(); i++) {
1325 View v = mBubbleContainer.getChildAt(i);
1326 if (v instanceof BadgedImageView
1327 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) {
1328 mBubbleContainer.removeViewAt(i);
Mady Mellore967e962020-03-26 17:36:44 -07001329 bubble.cleanupViews();
Lyn Hane1395572020-03-23 15:48:54 -07001330 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
1331 return;
1332 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001333 }
Lyn Hane1395572020-03-23 15:48:54 -07001334 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
Lyn Hanb58c7562020-01-07 14:29:20 -08001335 }
1336
Lyn Hanb4b06132020-05-11 09:25:20 -07001337 private void updateOverflowBtnVisibility() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001338 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1339 return;
1340 }
Lyn Hanb58c7562020-01-07 14:29:20 -08001341 if (mIsExpanded) {
1342 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001343 Log.d(TAG, "Show overflow button.");
Lyn Hanb58c7562020-01-07 14:29:20 -08001344 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001345 mBubbleOverflow.setBtnVisible(VISIBLE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001346 } else {
1347 if (DEBUG_BUBBLE_STACK_VIEW) {
1348 Log.d(TAG, "Collapsed. Hide overflow button.");
1349 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001350 mBubbleOverflow.setBtnVisible(GONE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001351 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001352 }
1353
1354 // via BubbleData.Listener
1355 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -04001356 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001357 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001358 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001359 }
1360
Mark Renoufba5ab512019-05-02 15:21:01 -04001361 public void updateBubbleOrder(List<Bubble> bubbles) {
1362 for (int i = 0; i < bubbles.size(); i++) {
1363 Bubble bubble = bubbles.get(i);
Mady Mellored99c272019-06-13 15:58:30 -07001364 mBubbleContainer.reorderView(bubble.getIconView(), i);
Mark Renoufba5ab512019-05-02 15:21:01 -04001365 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001366 updateBubbleZOrdersAndDotPosition(false /* animate */);
Lyn Hanf44562b2020-03-30 16:40:46 -07001367 updatePointerPosition();
Mark Renoufba5ab512019-05-02 15:21:01 -04001368 }
1369
Mady Melloredd4ee12019-01-18 10:45:11 -08001370 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001371 * Changes the currently selected bubble. If the stack is already expanded, the newly selected
1372 * bubble will be shown immediately. This does not change the expanded state or change the
1373 * position of any bubble.
1374 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001375 // via BubbleData.Listener
Lyn Han3cd75d72020-02-15 19:10:12 -08001376 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001377 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001378 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
1379 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001380 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
1381 return;
1382 }
Lyn Han89fb39d2020-04-07 11:51:07 -07001383 if (bubbleToSelect == null || bubbleToSelect.getKey() != BubbleOverflow.KEY) {
1384 mBubbleData.setShowingOverflow(false);
Lyn Han9dc38ec2020-05-06 10:50:16 -07001385 } else {
1386 mBubbleData.setShowingOverflow(true);
Lyn Han89fb39d2020-04-07 11:51:07 -07001387 }
Lyn Han9dc38ec2020-05-06 10:50:16 -07001388
Lyn Han3cd75d72020-02-15 19:10:12 -08001389 final BubbleViewProvider previouslySelected = mExpandedBubble;
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001390 mExpandedBubble = bubbleToSelect;
Lyn Han89fb39d2020-04-07 11:51:07 -07001391 updatePointerPosition();
Issei Suzukicac2a502019-04-16 16:52:50 +02001392
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001393 if (mIsExpanded) {
1394 // Make the container of the expanded view transparent before removing the expanded view
1395 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
1396 // expanded view becomes visible on the screen. See b/126856255
1397 mExpandedViewContainer.setAlpha(0.0f);
1398 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Lyn Han3cd75d72020-02-15 19:10:12 -08001399 previouslySelected.setContentVisibility(false);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001400 updateExpandedBubble();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001401 requestUpdate();
Lyn Han3cd75d72020-02-15 19:10:12 -08001402
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001403 logBubbleEvent(previouslySelected,
1404 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
1405 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor99a302602019-06-14 11:39:56 -07001406 notifyExpansionChanged(previouslySelected, false /* expanded */);
1407 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001408 });
1409 }
1410 }
1411
1412 /**
1413 * Changes the expanded state of the stack.
1414 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001415 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001416 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001417 // via BubbleData.Listener
1418 public void setExpanded(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001419 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001420 Log.d(TAG, "setExpanded: " + shouldExpand);
1421 }
Lyn Han285ad302019-05-29 19:01:39 -07001422 if (shouldExpand == mIsExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001423 return;
1424 }
Joshua Tsujibe60a582020-03-23 17:17:26 -04001425
1426 mSysUiState
1427 .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
1428 .commitUpdate(mContext.getDisplayId());
1429
Lyn Han285ad302019-05-29 19:01:39 -07001430 if (mIsExpanded) {
1431 animateCollapse();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001432 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001433 } else {
Lyn Han285ad302019-05-29 19:01:39 -07001434 animateExpansion();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001435 // TODO: move next line to BubbleData
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001436 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
1437 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001438 }
Mady Mellor99a302602019-06-14 11:39:56 -07001439 notifyExpansionChanged(mExpandedBubble, mIsExpanded);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001440 }
1441
1442 /**
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001443 * If necessary, shows the user education view for the bubble stack. This appears the first
1444 * time a user taps on a bubble.
1445 *
1446 * @return true if user education was shown, false otherwise.
1447 */
1448 private boolean maybeShowStackUserEducation() {
1449 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001450 mUserEducationView.setAlpha(0);
1451 mUserEducationView.setVisibility(VISIBLE);
1452 // Post so we have height of mUserEducationView
1453 mUserEducationView.post(() -> {
1454 final int viewHeight = mUserEducationView.getHeight();
1455 PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
1456 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
1457 mUserEducationView.setTranslationY(translationY);
1458 mUserEducationView.animate()
1459 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1460 .setInterpolator(FAST_OUT_SLOW_IN)
1461 .alpha(1);
1462 });
1463 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
1464 return true;
1465 }
1466 return false;
1467 }
1468
1469 /**
1470 * If necessary, hides the user education view for the bubble stack.
1471 *
1472 * @param fromExpansion if true this indicates the hide is happening due to the bubble being
1473 * expanded, false if due to a touch outside of the bubble stack.
1474 */
1475 void hideStackUserEducation(boolean fromExpansion) {
1476 if (mShouldShowUserEducation
1477 && mUserEducationView.getVisibility() == VISIBLE
1478 && !mAnimatingEducationAway) {
1479 mAnimatingEducationAway = true;
1480 mUserEducationView.animate()
1481 .alpha(0)
1482 .setDuration(fromExpansion
1483 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1484 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1485 .withEndAction(() -> {
1486 mAnimatingEducationAway = false;
1487 mShouldShowUserEducation = shouldShowBubblesEducation();
1488 mUserEducationView.setVisibility(GONE);
1489 });
1490 }
1491 }
1492
1493 /**
1494 * If necessary, toggles the user education view for the manage button. This is shown when the
1495 * bubble stack is expanded for the first time.
1496 *
1497 * @param show whether the user education view should show or not.
1498 */
1499 void maybeShowManageEducation(boolean show) {
1500 if (mManageEducationView == null) {
1501 return;
1502 }
1503 if (show
1504 && mShouldShowManageEducation
1505 && mManageEducationView.getVisibility() != VISIBLE
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001506 && mIsExpanded
1507 && mExpandedBubble.getExpandedView() != null) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001508 mManageEducationView.setAlpha(0);
1509 mManageEducationView.setVisibility(VISIBLE);
1510 mManageEducationView.post(() -> {
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001511 mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001512 final int viewHeight = mManageEducationView.getManageViewHeight();
1513 final int inset = getResources().getDimensionPixelSize(
1514 R.dimen.bubbles_manage_education_top_inset);
1515 mManageEducationView.bringToFront();
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001516 mManageEducationView.setManageViewPosition(mTempRect.left,
1517 mTempRect.top - viewHeight + inset);
1518 mManageEducationView.setPointerPosition(mTempRect.centerX() - mTempRect.left);
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001519 mManageEducationView.animate()
1520 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1521 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
1522 });
1523 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
1524 } else if (!show
1525 && mManageEducationView.getVisibility() == VISIBLE
1526 && !mAnimatingManageEducationAway) {
1527 mManageEducationView.animate()
1528 .alpha(0)
1529 .setDuration(mIsExpansionAnimating
1530 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1531 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1532 .withEndAction(() -> {
1533 mAnimatingManageEducationAway = false;
1534 mShouldShowManageEducation = shouldShowManageEducation();
1535 mManageEducationView.setVisibility(GONE);
1536 });
1537 }
1538 }
1539
1540 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001541 * Dismiss the stack of bubbles.
Lyn Han1b4f25e2019-06-11 13:56:34 -07001542 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001543 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001544 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001545 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -07001546 void stackDismissed(int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001547 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001548 Log.d(TAG, "stackDismissed: reason=" + reason);
1549 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001550 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -05001551 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001552 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001553 }
1554
1555 /**
Joshua Tsujif5c6a9c2020-02-25 17:47:59 -05001556 * @deprecated use {@link #setExpanded(boolean)} and
1557 * {@link BubbleData#setSelectedBubble(Bubble)}
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001558 */
1559 @Deprecated
1560 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -08001561 void collapseStack(Runnable endRunnable) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001562 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001563 Log.d(TAG, "collapseStack(endRunnable)");
1564 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001565 mBubbleData.setExpanded(false);
Mady Mellor9801e852019-01-22 14:50:28 -08001566 // TODO - use the runnable at end of animation
1567 endRunnable.run();
1568 }
1569
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001570 void showExpandedViewContents(int displayId) {
1571 if (mExpandedBubble != null
Mady Melloradd5c6a92020-03-31 17:22:48 -07001572 && mExpandedBubble.getExpandedView() != null
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001573 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
1574 mExpandedBubble.setContentVisibility(true);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001575 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001576 }
1577
Lyn Han285ad302019-05-29 19:01:39 -07001578 private void beforeExpandedViewAnimation() {
1579 hideFlyoutImmediate();
1580 updateExpandedBubble();
1581 updateExpandedView();
1582 mIsExpansionAnimating = true;
1583 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001584
Lyn Han285ad302019-05-29 19:01:39 -07001585 private void afterExpandedViewAnimation() {
1586 updateExpandedView();
1587 mIsExpansionAnimating = false;
1588 requestUpdate();
1589 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001590
Lyn Han285ad302019-05-29 19:01:39 -07001591 private void animateCollapse() {
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001592 // Hide the menu if it's visible.
1593 showManageMenu(false);
1594
Lyn Han285ad302019-05-29 19:01:39 -07001595 mIsExpanded = false;
Lyn Han3cd75d72020-02-15 19:10:12 -08001596 final BubbleViewProvider previouslySelected = mExpandedBubble;
Lyn Han285ad302019-05-29 19:01:39 -07001597 beforeExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001598 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001599
Lyn Hanb58c7562020-01-07 14:29:20 -08001600 if (DEBUG_BUBBLE_STACK_VIEW) {
1601 Log.d(TAG, "animateCollapse");
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001602 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
Lyn Han9f66c3b2020-03-05 23:59:29 -08001603 mExpandedBubble));
Lyn Hanb58c7562020-01-07 14:29:20 -08001604 }
Lyn Hanb4b06132020-05-11 09:25:20 -07001605 updateOverflowBtnVisibility();
Lyn Han285ad302019-05-29 19:01:39 -07001606 mBubbleContainer.cancelAllAnimations();
1607 mExpandedAnimationController.collapseBackToStack(
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001608 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
1609 /* collapseTo */,
Lyn Han285ad302019-05-29 19:01:39 -07001610 () -> {
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001611 mBubbleContainer.setActiveController(mStackAnimationController);
Lyn Han285ad302019-05-29 19:01:39 -07001612 afterExpandedViewAnimation();
Lyn Han3cd75d72020-02-15 19:10:12 -08001613 previouslySelected.setContentVisibility(false);
Lyn Han285ad302019-05-29 19:01:39 -07001614 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001615
Lyn Han285ad302019-05-29 19:01:39 -07001616 mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
1617 mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
1618 mExpandedViewContainer.animate()
1619 .setDuration(100)
1620 .alpha(0f);
1621 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001622
Lyn Han285ad302019-05-29 19:01:39 -07001623 private void animateExpansion() {
1624 mIsExpanded = true;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001625 hideStackUserEducation(true /* fromExpansion */);
Lyn Han285ad302019-05-29 19:01:39 -07001626 beforeExpandedViewAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001627
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001628 mBubbleContainer.setActiveController(mExpandedAnimationController);
Lyn Hanb4b06132020-05-11 09:25:20 -07001629 updateOverflowBtnVisibility();
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001630 mExpandedAnimationController.expandFromStack(() -> {
1631 updatePointerPosition();
1632 afterExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001633 maybeShowManageEducation(true);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001634 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001635
Lyn Han285ad302019-05-29 19:01:39 -07001636 mExpandedViewContainer.setTranslationX(getCollapsedX());
1637 mExpandedViewContainer.setTranslationY(getCollapsedY());
1638 mExpandedViewContainer.setAlpha(0f);
1639
1640 mExpandedViewXAnim.animateToFinalPosition(0f);
1641 mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1642 mExpandedViewContainer.animate()
1643 .setDuration(100)
1644 .alpha(1f);
1645 }
1646
1647 private float getCollapsedX() {
1648 return mStackAnimationController.getStackPosition().x < getWidth() / 2
1649 ? -mExpandedAnimateXDistance
1650 : mExpandedAnimateXDistance;
1651 }
1652
1653 private float getCollapsedY() {
1654 return Math.min(mStackAnimationController.getStackPosition().y,
1655 mExpandedAnimateYDistance);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001656 }
1657
Lyn Han3cd75d72020-02-15 19:10:12 -08001658 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
Mady Mellor99a302602019-06-14 11:39:56 -07001659 if (mExpandListener != null && bubble != null) {
1660 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001661 }
1662 }
1663
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001664 /** Return the BubbleView at the given index from the bubble container. */
Mady Mellorb8aaf972019-11-26 10:28:00 -08001665 public BadgedImageView getBubbleAt(int i) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001666 return getBubbleCount() > i
Mady Mellorb8aaf972019-11-26 10:28:00 -08001667 ? (BadgedImageView) mBubbleContainer.getChildAt(i)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001668 : null;
1669 }
1670
Joshua Tsujia19515f2019-02-13 18:02:29 -05001671 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
1672 public void onImeVisibilityChanged(boolean visible, int height) {
Mady Mellordf611cf2019-08-21 17:28:49 -07001673 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
Joshua Tsuji4b395912019-04-19 17:18:40 -04001674
Joshua Tsujiff6b0f22020-03-09 14:55:19 -04001675 if (!mIsExpanded && getBubbleCount() > 0) {
1676 final float stackDestinationY =
1677 mStackAnimationController.animateForImeVisibility(visible);
1678
1679 // How far the stack is animating due to IME, we'll just animate the flyout by that
1680 // much too.
1681 final float stackDy =
1682 stackDestinationY - mStackAnimationController.getStackPosition().y;
1683
1684 // If the flyout is visible, translate it along with the bubble stack.
1685 if (mFlyout.getVisibility() == VISIBLE) {
1686 PhysicsAnimator.getInstance(mFlyout)
1687 .spring(DynamicAnimation.TRANSLATION_Y,
1688 mFlyout.getTranslationY() + stackDy,
1689 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
1690 .start();
1691 }
Joshua Tsujia19515f2019-02-13 18:02:29 -05001692 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001693 }
1694
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001695 /**
1696 * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a
1697 * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV).
1698 * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided
1699 * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to
1700 * the special nature of ActivityView, it does not respect the standard
1701 * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for
1702 * this purpose.
1703 *
1704 * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation
1705 * properties for performance reasons. This means that the default implementation of this method
1706 * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in
1707 * it not receiving any touch events. This was previously addressed by returning false in the
1708 * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any
1709 * touch handlers in the stack or its child views.
1710 *
1711 * To support touch handlers, we're overriding this method to leave the ActivityView's touchable
1712 * region alone. The only touchable part of the stack that can ever overlap the AV is a
1713 * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually
1714 * updating the touchable region to allow users to grab a bubble while it completes its ~50ms
1715 * animation back to the bubble row.
1716 *
1717 * NOTE: Any future additions to the stack that obscure the ActivityView region will need their
1718 * bounds subtracted here in order to receive touch events.
1719 */
1720 @Override
1721 public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001722 // If the notification shade is expanded, or the manage menu is open, we shouldn't let the
1723 // ActivityView steal any touch events from any location.
1724 if (mNotificationShadeWindowController.getPanelExpanded() || mShowingManage) {
Joshua Tsujiba9fef02020-04-09 17:40:35 -04001725 touchableRegion.setEmpty();
1726 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001727 }
1728
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001729 /**
1730 * If you're here because you're not receiving touch events on a view that is a descendant of
1731 * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the
1732 * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView
1733 * consumes all touch events within its bounds, even for views like the BubbleStackView that are
1734 * above it. It ignores typical view touch handling methods like this one and
1735 * dispatchTouchEvent.
1736 */
1737 @Override
1738 public boolean onInterceptTouchEvent(MotionEvent ev) {
1739 return super.onInterceptTouchEvent(ev);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001740 }
1741
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001742 @Override
1743 public boolean dispatchTouchEvent(MotionEvent ev) {
1744 boolean dispatched = super.dispatchTouchEvent(ev);
1745
1746 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned
1747 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will
1748 // then be passed to the new bubble, which will not consume them since it hasn't received an
1749 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler
1750 // until the current gesture ends with an ACTION_UP event.
1751 if (!dispatched && !mIsExpanded && mIsGestureInProgress) {
1752 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev);
Joshua Tsuji442b6272019-02-08 13:23:43 -05001753 }
1754
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001755 mIsGestureInProgress =
1756 ev.getAction() != MotionEvent.ACTION_UP
1757 && ev.getAction() != MotionEvent.ACTION_CANCEL;
1758
1759 return dispatched;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001760 }
1761
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001762 void setFlyoutStateForDragLength(float deltaX) {
Joshua Tsuji8e05aab2019-08-22 14:57:50 -04001763 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1764 // is continually called.
1765 if (mFlyout.getWidth() <= 0) {
1766 return;
1767 }
1768
Joshua Tsuji6549e702019-05-02 13:13:16 -04001769 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1770 mFlyoutDragDeltaX = deltaX;
1771
1772 final float collapsePercent =
1773 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1774 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1775
Lyn Han61d5d562019-07-01 17:39:38 -07001776 // Calculate how to translate the flyout if it has been dragged too far in either direction.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001777 float overscrollTranslation = 0f;
1778 if (collapsePercent < 0f || collapsePercent > 1f) {
1779 // Whether we are more than 100% transitioned to the dot.
1780 final boolean overscrollingPastDot = collapsePercent > 1f;
1781
1782 // Whether we are overscrolling physically to the left - this can either be pulling the
1783 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1784 // after it has already become the dot.
1785 final boolean overscrollingLeft =
1786 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001787 overscrollTranslation =
1788 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1789 * (overscrollingLeft ? -1 : 1)
1790 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001791 // Attenuate the smaller dot less than the larger flyout.
1792 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001793 }
1794
1795 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1796 }
1797
Joshua Tsuji20103542020-02-18 14:06:28 -05001798 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001799 private boolean passEventToMagnetizedObject(MotionEvent event) {
Joshua Tsuji20103542020-02-18 14:06:28 -05001800 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
1801 }
1802
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001803 /**
1804 * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the
1805 * stack, if we're collapsed.
1806 */
1807 private void dismissMagnetizedObject() {
1808 if (mIsExpanded) {
1809 final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001810 dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView));
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001811 } else {
1812 mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
1813 }
1814 }
1815
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001816 private void dismissBubbleIfExists(@Nullable Bubble bubble) {
Lyn Han2f6e89d2020-04-15 10:01:01 -07001817 if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001818 mBubbleData.notificationEntryRemoved(
Pinyao Ting3c930612020-05-19 00:26:03 +00001819 bubble.getKey(), BubbleController.DISMISS_USER_GESTURE);
Joshua Tsuji6855cab2020-04-16 01:05:39 -04001820 }
1821 }
1822
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001823 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1824 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1825 mDesaturateAndDarkenTargetView = targetView;
1826
1827 if (desaturateAndDarken) {
1828 // Use the animated paint for the bubbles.
1829 mDesaturateAndDarkenTargetView.setLayerType(
1830 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1831 mDesaturateAndDarkenAnimator.removeAllListeners();
1832 mDesaturateAndDarkenAnimator.start();
1833 } else {
1834 mDesaturateAndDarkenAnimator.removeAllListeners();
1835 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1836 @Override
1837 public void onAnimationEnd(Animator animation) {
1838 super.onAnimationEnd(animation);
1839 // Stop using the animated paint.
1840 resetDesaturationAndDarken();
1841 }
1842 });
1843 mDesaturateAndDarkenAnimator.reverse();
1844 }
1845 }
1846
1847 private void resetDesaturationAndDarken() {
1848 mDesaturateAndDarkenAnimator.removeAllListeners();
1849 mDesaturateAndDarkenAnimator.cancel();
1850 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1851 }
1852
Lyn Han634483c2019-06-28 16:52:47 -07001853 /** Animates in the dismiss target. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001854 private void springInDismissTargetMaybe() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001855 if (mShowingDismiss) {
1856 return;
1857 }
1858
1859 mShowingDismiss = true;
1860
Joshua Tsuji20103542020-02-18 14:06:28 -05001861 mDismissTargetContainer.bringToFront();
1862 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
1863 mDismissTargetContainer.setVisibility(VISIBLE);
1864
1865 mDismissTargetAnimator.cancel();
1866 mDismissTargetAnimator
1867 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
1868 .start();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001869 }
1870
1871 /**
1872 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1873 * were dragged into the target and encircled.
1874 */
Lyn Han634483c2019-06-28 16:52:47 -07001875 private void hideDismissTarget() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001876 if (!mShowingDismiss) {
1877 return;
1878 }
1879
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001880 mShowingDismiss = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001881
Joshua Tsuji20103542020-02-18 14:06:28 -05001882 mDismissTargetAnimator
1883 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
1884 mDismissTargetSpring)
1885 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
1886 .start();
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001887 }
1888
Joshua Tsuji6549e702019-05-02 13:13:16 -04001889 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1890 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1891 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001892 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1893 // faster.
1894 mFlyoutTransitionSpring.getSpring().setStiffness(
1895 (mBubbleToExpandAfterFlyoutCollapse != null)
1896 ? SpringForce.STIFFNESS_MEDIUM
1897 : SpringForce.STIFFNESS_LOW);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001898 mFlyoutTransitionSpring
1899 .setStartValue(mFlyoutDragDeltaX)
1900 .setStartVelocity(velX)
1901 .animateToFinalPosition(collapsed
1902 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1903 : 0f);
1904 }
1905
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001906 /**
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001907 * Calculates the y position of the expanded view when it is expanded.
1908 */
Lyn Han285ad302019-05-29 19:01:39 -07001909 float getExpandedViewY() {
Lyn Han4a8efe32019-05-30 09:43:27 -07001910 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001911 }
1912
1913 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001914 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1915 */
1916 @VisibleForTesting
1917 void animateInFlyoutForBubble(Bubble bubble) {
Mady Mellordf898fd2020-01-09 09:26:36 -08001918 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001919 final BadgedImageView bubbleView = bubble.getIconView();
Mady Mellordf898fd2020-01-09 09:26:36 -08001920 if (flyoutMessage == null
1921 || flyoutMessage.message == null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001922 || !bubble.showFlyout()
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001923 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
Mark Renoufc19b4732019-06-26 12:08:33 -04001924 || isExpanded()
1925 || mIsExpansionAnimating
Joshua Tsuji14e68552019-06-06 17:17:08 -04001926 || mIsGestureInProgress
Lyn Hanf1f2c332019-08-23 17:06:56 -07001927 || mBubbleToExpandAfterFlyoutCollapse != null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001928 || bubbleView == null) {
1929 if (bubbleView != null) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001930 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001931 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001932 // Skip the message if none exists, we're expanded or animating expansion, or we're
Lyn Hanf1f2c332019-08-23 17:06:56 -07001933 // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
Mark Renoufc19b4732019-06-26 12:08:33 -04001934 return;
1935 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001936
Lyn Hanf1f2c332019-08-23 17:06:56 -07001937 mFlyoutDragDeltaX = 0f;
1938 clearFlyoutOnHide();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001939 mAfterFlyoutHidden = () -> {
1940 // Null it out to ensure it runs once.
1941 mAfterFlyoutHidden = null;
1942
1943 if (mBubbleToExpandAfterFlyoutCollapse != null) {
1944 // User tapped on the flyout and we should expand
1945 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1946 mBubbleData.setExpanded(true);
1947 mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001948 }
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001949
1950 // Stop suppressing the dot now that the flyout has morphed into the dot.
1951 bubbleView.removeDotSuppressionFlag(
1952 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Lyn Hanf1f2c332019-08-23 17:06:56 -07001953 };
1954 mFlyout.setVisibility(INVISIBLE);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001955
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001956 // Suppress the dot when we are animating the flyout.
1957 bubbleView.addDotSuppressionFlag(
1958 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001959
Lyn Hanf1f2c332019-08-23 17:06:56 -07001960 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1961 post(() -> {
1962 // An auto-expanding bubble could have been posted during the time it takes to
1963 // layout.
1964 if (isExpanded()) {
1965 return;
1966 }
1967 final Runnable expandFlyoutAfterDelay = () -> {
1968 mAnimateInFlyout = () -> {
1969 mFlyout.setVisibility(VISIBLE);
1970 mFlyoutDragDeltaX =
1971 mStackAnimationController.isStackOnLeftSide()
1972 ? -mFlyout.getWidth()
1973 : mFlyout.getWidth();
1974 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1975 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001976 };
Lyn Hanf1f2c332019-08-23 17:06:56 -07001977 mFlyout.postDelayed(mAnimateInFlyout, 200);
1978 };
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001979
1980 if (bubble.getIconView() == null) {
1981 return;
1982 }
1983
Mady Mellordf898fd2020-01-09 09:26:36 -08001984 mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
1985 mStackAnimationController.getStackPosition(), getWidth(),
Lyn Hanf1f2c332019-08-23 17:06:56 -07001986 mStackAnimationController.isStackOnLeftSide(),
Mady Mellor05e860b2019-10-30 22:48:15 -07001987 bubble.getIconView().getDotColor() /* dotColor */,
Lyn Hanf1f2c332019-08-23 17:06:56 -07001988 expandFlyoutAfterDelay /* onLayoutComplete */,
Mady Mellorb8aaf972019-11-26 10:28:00 -08001989 mAfterFlyoutHidden,
1990 bubble.getIconView().getDotCenter(),
1991 !bubble.showDot());
Lyn Hanf1f2c332019-08-23 17:06:56 -07001992 mFlyout.bringToFront();
1993 });
Mark Renoufc19b4732019-06-26 12:08:33 -04001994 mFlyout.removeCallbacks(mHideFlyout);
1995 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001996 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001997 }
1998
1999 /** Hide the flyout immediately and cancel any pending hide runnables. */
2000 private void hideFlyoutImmediate() {
Lyn Hanf1f2c332019-08-23 17:06:56 -07002001 clearFlyoutOnHide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04002002 mFlyout.removeCallbacks(mAnimateInFlyout);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04002003 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04002004 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04002005 }
2006
Lyn Hanf1f2c332019-08-23 17:06:56 -07002007 private void clearFlyoutOnHide() {
2008 mFlyout.removeCallbacks(mAnimateInFlyout);
Mady Mellorb8aaf972019-11-26 10:28:00 -08002009 if (mAfterFlyoutHidden == null) {
Lyn Hanf1f2c332019-08-23 17:06:56 -07002010 return;
2011 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08002012 mAfterFlyoutHidden.run();
2013 mAfterFlyoutHidden = null;
Lyn Hanf1f2c332019-08-23 17:06:56 -07002014 }
2015
Joshua Tsujid9923e52020-04-29 15:33:01 -04002016 /**
2017 * Fills the Rect with the touchable region of the bubbles. This will be used by WindowManager
2018 * to decide which touch events go to Bubbles.
2019 *
2020 * Bubbles is below the status bar/notification shade but above application windows. If you're
2021 * trying to get touch events from the status bar or another higher-level window layer, you'll
2022 * need to re-order TYPE_BUBBLES in WindowManagerPolicy so that we have the opportunity to steal
2023 * them.
2024 */
2025 public void getTouchableRegion(Rect outRect) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08002026 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
2027 // When user education shows then capture all touches
2028 outRect.set(0, 0, getWidth(), getHeight());
2029 return;
2030 }
2031
Joshua Tsujib1a796b2019-01-16 15:43:12 -08002032 if (!mIsExpanded) {
Lyn Hanc47e1712020-01-28 21:43:34 -08002033 if (getBubbleCount() > 0) {
Mady Mellor217b2e92019-02-27 11:44:16 -08002034 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
Joshua Tsujid9923e52020-04-29 15:33:01 -04002035 // Increase the touch target size of the bubble
2036 outRect.top -= mBubbleTouchPadding;
2037 outRect.left -= mBubbleTouchPadding;
2038 outRect.right += mBubbleTouchPadding;
2039 outRect.bottom += mBubbleTouchPadding;
Mady Mellor217b2e92019-02-27 11:44:16 -08002040 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08002041 } else {
2042 mBubbleContainer.getBoundsOnScreen(outRect);
2043 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04002044
Joshua Tsuji6549e702019-05-02 13:13:16 -04002045 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04002046 final Rect flyoutBounds = new Rect();
2047 mFlyout.getBoundsOnScreen(flyoutBounds);
2048 outRect.union(flyoutBounds);
2049 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08002050 }
2051
2052 private int getStatusBarHeight() {
2053 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05002054 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08002055 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05002056 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05002057 insets.getDisplayCutout() != null
2058 ? insets.getDisplayCutout().getSafeInsetTop()
2059 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08002060 }
2061
2062 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002063 }
2064
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002065 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07002066 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002067 return;
2068 }
2069 mViewUpdatedRequested = true;
2070 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
2071 invalidate();
2072 }
2073
Joshua Tsuji6855cab2020-04-16 01:05:39 -04002074 private void showManageMenu(boolean show) {
2075 mShowingManage = show;
2076
2077 // This should not happen, since the manage menu is only visible when there's an expanded
2078 // bubble. If we end up in this state, just hide the menu immediately.
2079 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
2080 mManageMenu.setVisibility(View.INVISIBLE);
2081 return;
2082 }
2083
2084 // If available, update the manage menu's settings option with the expanded bubble's app
2085 // name and icon.
Lyn Han2f6e89d2020-04-15 10:01:01 -07002086 if (show && mBubbleData.hasBubbleInStackWithKey(mExpandedBubble.getKey())) {
2087 final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey());
Joshua Tsuji6855cab2020-04-16 01:05:39 -04002088 mManageSettingsIcon.setImageDrawable(bubble.getBadgedAppIcon());
2089 mManageSettingsText.setText(getResources().getString(
2090 R.string.bubbles_app_settings, bubble.getAppName()));
2091 }
2092
2093 mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
2094
2095 // When the menu is open, it should be at these coordinates. This will make the menu's
2096 // bottom left corner match up with the button's bottom left corner.
2097 final float targetX = mTempRect.left;
2098 final float targetY = mTempRect.bottom - mManageMenu.getHeight();
2099
2100 if (show) {
2101 mManageMenu.setScaleX(0.5f);
2102 mManageMenu.setScaleY(0.5f);
2103 mManageMenu.setTranslationX(targetX - mManageMenu.getWidth() / 4);
2104 mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4);
2105 mManageMenu.setAlpha(0f);
2106
2107 PhysicsAnimator.getInstance(mManageMenu)
2108 .spring(DynamicAnimation.ALPHA, 1f)
2109 .spring(DynamicAnimation.SCALE_X, 1f)
2110 .spring(DynamicAnimation.SCALE_Y, 1f)
2111 .spring(DynamicAnimation.TRANSLATION_X, targetX)
2112 .spring(DynamicAnimation.TRANSLATION_Y, targetY)
2113 .start();
2114
2115 mManageMenu.setVisibility(View.VISIBLE);
2116 } else {
2117 PhysicsAnimator.getInstance(mManageMenu)
2118 .spring(DynamicAnimation.ALPHA, 0f)
2119 .spring(DynamicAnimation.SCALE_X, 0.5f)
2120 .spring(DynamicAnimation.SCALE_Y, 0.5f)
2121 .spring(DynamicAnimation.TRANSLATION_X, targetX - mManageMenu.getWidth() / 4)
2122 .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4)
2123 .withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE))
2124 .start();
2125 }
2126
2127 // Update the AV's obscured touchable region for the new menu visibility state.
2128 mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
2129 }
2130
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002131 private void updateExpandedBubble() {
Issei Suzukia8d07312019-06-07 12:56:19 +02002132 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002133 Log.d(TAG, "updateExpandedBubble()");
2134 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08002135 mExpandedViewContainer.removeAllViews();
Mady Mellor2dce0fe2020-04-10 13:40:05 -07002136 if (mIsExpanded && mExpandedBubble != null
2137 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -08002138 BubbleExpandedView bev = mExpandedBubble.getExpandedView();
Lyn Hana0bb02e2020-01-28 17:57:27 -08002139 mExpandedViewContainer.addView(bev);
Joshua Tsuji6855cab2020-04-16 01:05:39 -04002140 bev.setManageClickListener((view) -> showManageMenu(!mShowingManage));
Lyn Hana0bb02e2020-01-28 17:57:27 -08002141 bev.populateExpandedView();
2142 mExpandedViewContainer.setVisibility(VISIBLE);
Issei Suzukic0387542019-03-08 17:31:14 +01002143 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002144 }
2145 }
2146
Lyn Han285ad302019-05-29 19:01:39 -07002147 private void updateExpandedView() {
Issei Suzukia8d07312019-06-07 12:56:19 +02002148 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Han285ad302019-05-29 19:01:39 -07002149 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002150 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04002151
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002152 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08002153 if (mIsExpanded) {
Lyn Han285ad302019-05-29 19:01:39 -07002154 final float y = getExpandedViewY();
Mady Mellor5d8f1402019-02-21 18:23:52 -08002155 if (!mExpandedViewYAnim.isRunning()) {
2156 // We're not animating so set the value
2157 mExpandedViewContainer.setTranslationY(y);
Mady Mellor2dce0fe2020-04-10 13:40:05 -07002158 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -08002159 mExpandedBubble.getExpandedView().updateView();
2160 }
Mady Mellor5d8f1402019-02-21 18:23:52 -08002161 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07002162 // We are animating so update the value; there is an end listener on the animator
2163 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08002164 mExpandedViewYAnim.animateToFinalPosition(y);
2165 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002166 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08002167
Joshua Tsuji6549e702019-05-02 13:13:16 -04002168 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04002169 updateBubbleZOrdersAndDotPosition(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04002170 }
2171
2172 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04002173 private void updateBubbleZOrdersAndDotPosition(boolean animate) {
Lyn Hanc47e1712020-01-28 21:43:34 -08002174 int bubbleCount = getBubbleCount();
Lyn Han1b4f25e2019-06-11 13:56:34 -07002175 for (int i = 0; i < bubbleCount; i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08002176 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellor70958542019-09-24 17:12:46 -07002177 bv.setZ((mMaxBubbles * mBubbleElevation) - i);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04002178
Joshua Tsuji6549e702019-05-02 13:13:16 -04002179 // If the dot is on the left, and so is the stack, we need to change the dot position.
2180 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04002181 bv.setDotPositionOnLeft(!mStackOnLeftOrWillBe, animate);
2182 }
2183
2184 if (!mIsExpanded && i > 0) {
2185 // If we're collapsed and this bubble is behind other bubbles, suppress its dot.
2186 bv.addDotSuppressionFlag(
2187 BadgedImageView.SuppressionFlag.BEHIND_STACK);
2188 } else {
2189 bv.removeDotSuppressionFlag(
2190 BadgedImageView.SuppressionFlag.BEHIND_STACK);
Joshua Tsuji6549e702019-05-02 13:13:16 -04002191 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002192 }
2193 }
2194
Mady Mellorde2d4d22019-01-29 14:15:34 -08002195 private void updatePointerPosition() {
Mady Mellor2dce0fe2020-04-10 13:40:05 -07002196 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
Lyn Han522e9ff2019-05-17 13:26:13 -07002197 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08002198 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08002199 int index = getBubbleIndex(mExpandedBubble);
Lyn Hanf44562b2020-03-30 16:40:46 -07002200 if (index == -1) {
2201 return;
2202 }
Lyn Han522e9ff2019-05-17 13:26:13 -07002203 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
2204 float halfBubble = mBubbleSize / 2f;
Mady Mellor9be3bed2019-08-21 17:26:26 -07002205 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
2206 // Padding might be adjusted for insets, so get it directly from the view
2207 bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
Lyn Han9f66c3b2020-03-05 23:59:29 -08002208 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08002209 }
2210
Steven Wua254dab2019-01-29 11:30:39 -05002211 /**
2212 * @return the number of bubbles in the stack view.
2213 */
Steven Wub00225b2019-02-08 14:27:42 -05002214 public int getBubbleCount() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08002215 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
2216 // Subtract 1 for the overflow button that is always in the bubble container.
2217 return mBubbleContainer.getChildCount() - 1;
2218 }
2219 return mBubbleContainer.getChildCount();
Steven Wua254dab2019-01-29 11:30:39 -05002220 }
2221
2222 /**
2223 * Finds the bubble index within the stack.
2224 *
Lyn Han3cd75d72020-02-15 19:10:12 -08002225 * @param provider the bubble view provider with the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05002226 * @return the index of the bubble view within the bubble stack. The range of the position
2227 * is between 0 and the bubble count minus 1.
2228 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002229 int getBubbleIndex(@Nullable BubbleViewProvider provider) {
Lyn Han9f66c3b2020-03-05 23:59:29 -08002230 if (provider == null) {
Steven Wua62cb6a2019-02-15 17:12:51 -05002231 return 0;
2232 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08002233 return mBubbleContainer.indexOfChild(provider.getIconView());
Steven Wua254dab2019-01-29 11:30:39 -05002234 }
2235
2236 /**
2237 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
2238 */
Steven Wub00225b2019-02-08 14:27:42 -05002239 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002240 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05002241 .setScale(4, RoundingMode.CEILING.HALF_UP)
2242 .floatValue();
2243 }
2244
2245 /**
2246 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
2247 */
Steven Wub00225b2019-02-08 14:27:42 -05002248 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002249 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05002250 .setScale(4, RoundingMode.CEILING.HALF_UP)
2251 .floatValue();
2252 }
2253
Joshua Tsujia19515f2019-02-13 18:02:29 -05002254 public PointF getStackPosition() {
2255 return mStackAnimationController.getStackPosition();
2256 }
2257
Steven Wua254dab2019-01-29 11:30:39 -05002258 /**
2259 * Logs the bubble UI event.
2260 *
2261 * @param bubble the bubble that is being interacted on. Null value indicates that
2262 * the user interaction is not specific to one bubble.
2263 * @param action the user interaction enum.
2264 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002265 private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
2266 if (bubble == null) {
2267 return;
Steven Wua254dab2019-01-29 11:30:39 -05002268 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002269 bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
2270 getNormalizedYPosition(), getBubbleIndex(bubble));
Steven Wua254dab2019-01-29 11:30:39 -05002271 }
Mark Renouf041d7262019-02-06 12:09:41 -05002272
2273 /**
2274 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
2275 * a back key down/up event pair is forwarded to the bubble Activity.
2276 */
2277 boolean performBackPressIfNeeded() {
Mady Mellor2dce0fe2020-04-10 13:40:05 -07002278 if (!isExpanded() || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
Mark Renouf041d7262019-02-06 12:09:41 -05002279 return false;
2280 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002281 return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
Mark Renouf041d7262019-02-06 12:09:41 -05002282 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002283
Mady Mellor5a3e94b2020-02-07 12:16:21 -08002284 /** Whether the educational view should appear for bubbles. **/
2285 private boolean shouldShowBubblesEducation() {
2286 return BubbleDebugConfig.forceShowUserEducation(getContext())
2287 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
2288 }
2289
2290 /** Whether the educational view should appear for the expanded view "manage" button. **/
2291 private boolean shouldShowManageEducation() {
2292 return BubbleDebugConfig.forceShowUserEducation(getContext())
2293 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
2294 }
2295
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002296 /** For debugging only */
2297 List<Bubble> getBubblesOnScreen() {
2298 List<Bubble> bubbles = new ArrayList<>();
Lyn Hanc47e1712020-01-28 21:43:34 -08002299 for (int i = 0; i < getBubbleCount(); i++) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002300 View child = mBubbleContainer.getChildAt(i);
Mady Mellorb8aaf972019-11-26 10:28:00 -08002301 if (child instanceof BadgedImageView) {
2302 String key = ((BadgedImageView) child).getKey();
Lyn Han2f6e89d2020-04-15 10:01:01 -07002303 Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002304 bubbles.add(bubble);
2305 }
2306 }
2307 return bubbles;
2308 }
Joshua Tsuji6855cab2020-04-16 01:05:39 -04002309
2310 /**
2311 * Logs bubble UI click event.
2312 *
2313 * @param bubble the bubble notification entry that user is interacting with.
2314 * @param action the user interaction enum.
2315 */
2316 private void logBubbleClickEvent(Bubble bubble, int action) {
Pinyao Ting3c930612020-05-19 00:26:03 +00002317 bubble.logUIEvent(
Joshua Tsuji6855cab2020-04-16 01:05:39 -04002318 getBubbleCount(),
2319 action,
2320 getNormalizedXPosition(),
2321 getNormalizedYPosition(),
Pinyao Ting3c930612020-05-19 00:26:03 +00002322 getBubbleIndex(getExpandedBubble())
2323 );
Joshua Tsuji6855cab2020-04-16 01:05:39 -04002324 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002325}