blob: 0aabdff96e1e82d475bd947239cdac4aa2518596 [file] [log] [blame]
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.bubbles;
18
Joshua Tsujib1a796b2019-01-16 15:43:12 -080019import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080020import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21
Mady Mellor5a3e94b2020-02-07 12:16:21 -080022import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
23import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_EDUCATION;
24import static com.android.systemui.Prefs.Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION;
Issei Suzukia8d07312019-06-07 12:56:19 +020025import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080026import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION;
Issei Suzukia8d07312019-06-07 12:56:19 +020027import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
28import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
29
Joshua Tsuji4accf5982019-04-22 17:36:11 -040030import android.animation.Animator;
31import android.animation.AnimatorListenerAdapter;
32import android.animation.ValueAnimator;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040033import android.annotation.SuppressLint;
Lyn Han6c40fe72019-05-08 14:06:33 -070034import android.app.Notification;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080035import android.content.Context;
Lyn Hanf4730312019-06-18 11:18:58 -070036import android.content.res.Configuration;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080037import android.content.res.Resources;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080038import android.content.res.TypedArray;
39import android.graphics.Color;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040040import android.graphics.ColorMatrix;
41import android.graphics.ColorMatrixColorFilter;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040042import android.graphics.Paint;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080043import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080044import android.graphics.PointF;
45import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080046import android.graphics.RectF;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040047import android.graphics.Region;
Mady Mellor217b2e92019-02-27 11:44:16 -080048import android.os.Bundle;
Joshua Tsuji4accf5982019-04-22 17:36:11 -040049import android.os.Vibrator;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050050import android.util.Log;
Issei Suzukic0387542019-03-08 17:31:14 +010051import android.view.Choreographer;
Mady Mellor9be3bed2019-08-21 17:26:26 -070052import android.view.DisplayCutout;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -040053import android.view.Gravity;
Mady Mellordea7ecf2018-12-10 15:47:40 -080054import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080055import android.view.MotionEvent;
56import android.view.View;
Joshua Tsuji20103542020-02-18 14:06:28 -050057import android.view.ViewGroup;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080058import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050059import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080060import android.view.WindowManager;
Mady Mellor217b2e92019-02-27 11:44:16 -080061import android.view.accessibility.AccessibilityNodeInfo;
Lyn Hane68d0912019-05-02 18:28:01 -070062import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040063import android.view.animation.AccelerateDecelerateInterpolator;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080064import android.widget.FrameLayout;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080065import android.widget.TextView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080066
Mark Renoufcecc77b2019-01-30 16:32:24 -050067import androidx.annotation.MainThread;
Joshua Tsuji20103542020-02-18 14:06:28 -050068import androidx.annotation.NonNull;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080069import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080070import androidx.dynamicanimation.animation.DynamicAnimation;
Joshua Tsuji6549e702019-05-02 13:13:16 -040071import androidx.dynamicanimation.animation.FloatPropertyCompat;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080072import androidx.dynamicanimation.animation.SpringAnimation;
73import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080074
Mady Melloredd4ee12019-01-18 10:45:11 -080075import com.android.internal.annotations.VisibleForTesting;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080076import com.android.internal.util.ContrastColorUtil;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080077import com.android.internal.widget.ViewClippingUtil;
Mady Mellor5a3e94b2020-02-07 12:16:21 -080078import com.android.systemui.Prefs;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080079import com.android.systemui.R;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080080import com.android.systemui.bubbles.animation.ExpandedAnimationController;
81import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
82import com.android.systemui.bubbles.animation.StackAnimationController;
Joshua Tsujibe60a582020-03-23 17:17:26 -040083import com.android.systemui.model.SysUiState;
84import com.android.systemui.shared.system.QuickStepContract;
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -080085import com.android.systemui.shared.system.SysUiStatsLog;
Joshua Tsujiba9fef02020-04-09 17:40:35 -040086import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
Joshua Tsuji20103542020-02-18 14:06:28 -050087import com.android.systemui.util.DismissCircleView;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050088import com.android.systemui.util.FloatingContentCoordinator;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040089import com.android.systemui.util.RelativeTouchListener;
Joshua Tsuji20103542020-02-18 14:06:28 -050090import com.android.systemui.util.animation.PhysicsAnimator;
91import com.android.systemui.util.magnetictarget.MagnetizedObject;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080092
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040093import java.io.FileDescriptor;
94import java.io.PrintWriter;
Steven Wua254dab2019-01-29 11:30:39 -050095import java.math.BigDecimal;
96import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040097import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -040098import java.util.Collections;
99import java.util.List;
Steven Wua254dab2019-01-29 11:30:39 -0500100
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800101/**
102 * Renders bubbles in a stack and handles animating expanded and collapsed states.
103 */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500104public class BubbleStackView extends FrameLayout {
Issei Suzukia8d07312019-06-07 12:56:19 +0200105 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800106
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800107 /** Animation durations for bubble stack user education views. **/
108 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
109 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
110
Joshua Tsuji6549e702019-05-02 13:13:16 -0400111 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
112 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
113
114 /** Velocity required to dismiss the flyout via drag. */
115 private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
116
117 /**
118 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
119 * for every 8 pixels overscrolled).
120 */
121 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
122
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400123 /** Duration of the flyout alpha animations. */
124 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
125
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400126 /** Percent to darken the bubbles when they're in the dismiss target. */
127 private static final float DARKEN_PERCENT = 0.3f;
128
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400129 /** How long to wait, in milliseconds, before hiding the flyout. */
130 @VisibleForTesting
131 static final int FLYOUT_HIDE_AFTER = 5000;
132
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400133 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
134 new PhysicsAnimator.SpringConfig(
135 StackAnimationController.IME_ANIMATION_STIFFNESS,
136 StackAnimationController.DEFAULT_BOUNCINESS);
137
Issei Suzukic0387542019-03-08 17:31:14 +0100138 /**
139 * Interface to synchronize {@link View} state and the screen.
140 *
141 * {@hide}
142 */
143 interface SurfaceSynchronizer {
144 /**
145 * Wait until requested change on a {@link View} is reflected on the screen.
146 *
147 * @param callback callback to run after the change is reflected on the screen.
148 */
149 void syncSurfaceAndRun(Runnable callback);
150 }
151
152 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
153 new SurfaceSynchronizer() {
154 @Override
155 public void syncSurfaceAndRun(Runnable callback) {
156 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
157 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
158 // the requested change is reflected on the screen.
159 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
160 // surfaces, rewrite this logic with them.
161 private int mFrameWait = 2;
162
163 @Override
164 public void doFrame(long frameTimeNanos) {
165 if (--mFrameWait > 0) {
166 Choreographer.getInstance().postFrameCallback(this);
167 } else {
168 callback.run();
169 }
170 }
171 });
172 }
173 };
174
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800175 private Point mDisplaySize;
176
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800177 private final SpringAnimation mExpandedViewXAnim;
178 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800179 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800180
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400181 private final Vibrator mVibrator;
182 private final ValueAnimator mDesaturateAndDarkenAnimator;
183 private final Paint mDesaturateAndDarkenPaint = new Paint();
184
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800185 private PhysicsAnimationLayout mBubbleContainer;
186 private StackAnimationController mStackAnimationController;
187 private ExpandedAnimationController mExpandedAnimationController;
188
Mady Mellor3dff9e62019-02-05 18:12:53 -0800189 private FrameLayout mExpandedViewContainer;
190
Joshua Tsuji6549e702019-05-02 13:13:16 -0400191 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400192 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400193 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700194 /**
195 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
196 * previous one animates out.
197 */
Mady Mellorb8aaf972019-11-26 10:28:00 -0800198 private Runnable mAfterFlyoutHidden;
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800199 /**
200 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
201 * once it collapses.
202 */
203 @Nullable
204 private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400205
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400206 /** Layout change listener that moves the stack to the nearest valid position on rotation. */
Lyn Hanf4730312019-06-18 11:18:58 -0700207 private OnLayoutChangeListener mOrientationChangedListener;
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400208 /** Whether the stack was on the left side of the screen prior to rotation. */
209 private boolean mWasOnLeftBeforeRotation = false;
210 /**
211 * How far down the screen the stack was before rotation, in terms of percentage of the way down
212 * the allowable region. Defaults to -1 if not set.
213 */
214 private float mVerticalPosPercentBeforeRotation = -1;
215
Mady Mellor70958542019-09-24 17:12:46 -0700216 private int mMaxBubbles;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800217 private int mBubbleSize;
Mady Mellor70958542019-09-24 17:12:46 -0700218 private int mBubbleElevation;
Lyn Han4a8efe32019-05-30 09:43:27 -0700219 private int mBubblePaddingTop;
Mady Mellore9371bc2019-07-10 18:50:59 -0700220 private int mBubbleTouchPadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700221 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800222 private int mExpandedAnimateXDistance;
223 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700224 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500225 private int mStatusBarHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500226 private int mImeOffset;
Lyn Han3cd75d72020-02-15 19:10:12 -0800227 private BubbleViewProvider mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800228 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800229
Joshua Tsuji6549e702019-05-02 13:13:16 -0400230 /** Whether the stack is currently on the left side of the screen, or animating there. */
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400231 private boolean mStackOnLeftOrWillBe = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400232
233 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
234 private boolean mIsGestureInProgress = false;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400235
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400236 /** Description of current animation controller state. */
237 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
238 pw.println("Stack view state:");
239 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
240 pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
241 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400242 mStackAnimationController.dump(fd, pw, args);
243 mExpandedAnimationController.dump(fd, pw, args);
244 }
245
Mady Mellorcd9b1302018-11-06 18:08:04 -0800246 private BubbleController.BubbleExpandListener mExpandListener;
Joshua Tsujibe60a582020-03-23 17:17:26 -0400247 private SysUiState mSysUiState;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800248
249 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700250 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400251 private boolean mShowingDismiss = false;
252
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400253 /** The view to desaturate/darken when magneted to the dismiss target. */
254 private View mDesaturateAndDarkenTargetView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800255
Mady Mellor3dff9e62019-02-05 18:12:53 -0800256 private LayoutInflater mInflater;
257
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800258 // Used for determining view / touch intersection
259 int[] mTempLoc = new int[2];
260 RectF mTempRect = new RectF();
261
Mark Renouf821e6782019-04-01 14:17:37 -0400262 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
263
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800264 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
265 new ViewTreeObserver.OnPreDrawListener() {
266 @Override
267 public boolean onPreDraw() {
268 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Lyn Han285ad302019-05-29 19:01:39 -0700269 updateExpandedView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800270 mViewUpdatedRequested = false;
271 return true;
272 }
273 };
274
Mark Renouf821e6782019-04-01 14:17:37 -0400275 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
276 this::updateSystemGestureExcludeRects;
277
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800278 private ViewClippingUtil.ClippingParameters mClippingParameters =
279 new ViewClippingUtil.ClippingParameters() {
280
Lyn Han522e9ff2019-05-17 13:26:13 -0700281 @Override
282 public boolean shouldFinish(View view) {
283 return false;
284 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800285
Lyn Han522e9ff2019-05-17 13:26:13 -0700286 @Override
287 public boolean isClippingEnablingAllowed(View view) {
288 return !mIsExpanded;
289 }
290 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800291
Joshua Tsuji6549e702019-05-02 13:13:16 -0400292 /** Float property that 'drags' the flyout. */
293 private final FloatPropertyCompat mFlyoutCollapseProperty =
294 new FloatPropertyCompat("FlyoutCollapseSpring") {
295 @Override
296 public float getValue(Object o) {
297 return mFlyoutDragDeltaX;
298 }
299
300 @Override
301 public void setValue(Object o, float v) {
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400302 setFlyoutStateForDragLength(v);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400303 }
304 };
305
306 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
307 private final SpringAnimation mFlyoutTransitionSpring =
308 new SpringAnimation(this, mFlyoutCollapseProperty);
309
310 /** Distance the flyout has been dragged in the X axis. */
311 private float mFlyoutDragDeltaX = 0f;
312
313 /**
Joshua Tsuji14e68552019-06-06 17:17:08 -0400314 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
315 */
316 private Runnable mAnimateInFlyout;
317
318 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -0400319 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
320 * it immediately.
321 */
322 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
323 (dynamicAnimation, b, v, v1) -> {
324 if (mFlyoutDragDeltaX == 0) {
325 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
326 } else {
327 mFlyout.hideFlyout();
328 }
329 };
330
Lyn Han1b4f25e2019-06-11 13:56:34 -0700331 @NonNull
332 private final SurfaceSynchronizer mSurfaceSynchronizer;
Issei Suzukic0387542019-03-08 17:31:14 +0100333
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400334 private final NotificationShadeWindowController mNotificationShadeWindowController;
335
Joshua Tsuji20103542020-02-18 14:06:28 -0500336 /**
337 * The currently magnetized object, which is being dragged and will be attracted to the magnetic
338 * dismiss target.
339 *
340 * This is either the stack itself, or an individual bubble.
341 */
342 private MagnetizedObject<?> mMagnetizedObject;
343
344 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500345 * The MagneticTarget instance for our circular dismiss view. This is added to the
346 * MagnetizedObject instances for the stack and any dragged-out bubbles.
347 */
348 private MagnetizedObject.MagneticTarget mMagneticTarget;
349
350 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
351 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
352 new MagnetizedObject.MagnetListener() {
353 @Override
354 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
355 animateDesaturateAndDarken(
356 mExpandedAnimationController.getDraggedOutBubble(), true);
357 }
358
359 @Override
360 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
361 float velX, float velY, boolean wasFlungOut) {
362 animateDesaturateAndDarken(
363 mExpandedAnimationController.getDraggedOutBubble(), false);
364
365 if (wasFlungOut) {
366 mExpandedAnimationController.snapBubbleBack(
367 mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
368 hideDismissTarget();
369 } else {
370 mExpandedAnimationController.onUnstuckFromTarget();
371 }
372 }
373
374 @Override
375 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
376 mExpandedAnimationController.dismissDraggedOutBubble(
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400377 mExpandedAnimationController.getDraggedOutBubble() /* bubble */,
378 mDismissTargetContainer.getHeight() /* translationYBy */,
379 BubbleStackView.this::dismissMagnetizedObject /* after */);
Joshua Tsuji20103542020-02-18 14:06:28 -0500380 hideDismissTarget();
381 }
382 };
383
384 /** Magnet listener that handles animating and dismissing the entire stack. */
385 private final MagnetizedObject.MagnetListener mStackMagnetListener =
386 new MagnetizedObject.MagnetListener() {
387 @Override
388 public void onStuckToTarget(
389 @NonNull MagnetizedObject.MagneticTarget target) {
390 animateDesaturateAndDarken(mBubbleContainer, true);
391 }
392
393 @Override
394 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
395 float velX, float velY, boolean wasFlungOut) {
396 animateDesaturateAndDarken(mBubbleContainer, false);
397
398 if (wasFlungOut) {
399 mStackAnimationController.flingStackThenSpringToEdge(
400 mStackAnimationController.getStackPosition().x, velX, velY);
401 hideDismissTarget();
402 } else {
403 mStackAnimationController.onUnstuckFromTarget();
404 }
405 }
406
407 @Override
408 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400409 mStackAnimationController.animateStackDismissal(
410 mDismissTargetContainer.getHeight() /* translationYBy */,
Joshua Tsuji20103542020-02-18 14:06:28 -0500411 () -> {
412 resetDesaturationAndDarken();
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400413 dismissMagnetizedObject();
Joshua Tsuji20103542020-02-18 14:06:28 -0500414 }
415 );
416
417 hideDismissTarget();
418 }
419 };
420
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400421 /**
422 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack.
423 * When expanded, clicking a bubble either expands that bubble, or collapses the stack.
424 */
425 private OnClickListener mBubbleClickListener = new OnClickListener() {
426 @Override
427 public void onClick(View view) {
428 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view);
429
430 // If the bubble has since left us, ignore the click.
431 if (clickedBubble == null) {
432 return;
433 }
434
435 final boolean clickedBubbleIsCurrentlyExpandedBubble =
436 clickedBubble.getKey().equals(mExpandedBubble.getKey());
437
438 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) {
439 if (clickedBubble != mBubbleData.getSelectedBubble()) {
440 // Select the clicked bubble.
441 mBubbleData.setSelectedBubble(clickedBubble);
442 } else {
443 // If the clicked bubble is the selected bubble (but not the expanded bubble),
444 // that means overflow was previously expanded. Set the selected bubble
445 // internally without going through BubbleData (which would ignore it since it's
446 // already selected).
Lyn Han89fb39d2020-04-07 11:51:07 -0700447 mBubbleData.setShowingOverflow(true);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400448 setSelectedBubble(clickedBubble);
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400449 }
450 } else {
451 // Otherwise, we either tapped the stack (which means we're collapsed
452 // and should expand) or the currently selected bubble (we're expanded
453 // and should collapse).
454 if (!maybeShowStackUserEducation()) {
455 mBubbleData.setExpanded(!mBubbleData.isExpanded());
456 }
457 }
458 }
459 };
460
461 /**
462 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when
463 * collapsed), or individual bubbles (when expanded).
464 */
465 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() {
466
467 @Override
468 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
469 // If we're expanding or collapsing, consume but ignore all touch events.
470 if (mIsExpansionAnimating) {
471 return true;
472 }
473
474 if (mBubbleData.isExpanded()) {
475 maybeShowManageEducation(false /* show */);
476
477 // If we're expanded, tell the animation controller to prepare to drag this bubble,
478 // dispatching to the individual bubble magnet listener.
479 mExpandedAnimationController.prepareForBubbleDrag(
480 v /* bubble */,
481 mMagneticTarget,
482 mIndividualBubbleMagnetListener);
483
484 // Save the magnetized individual bubble so we can dispatch touch events to it.
485 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
486 } else {
487 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the
488 // animation controller, and hide the flyout.
489 mStackAnimationController.cancelStackPositionAnimations();
490 mBubbleContainer.setActiveController(mStackAnimationController);
491 hideFlyoutImmediate();
492
493 // Also, save the magnetized stack so we can dispatch touch events to it.
494 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
495 mMagnetizedObject.setMagnetListener(mStackMagnetListener);
496 }
497
498 passEventToMagnetizedObject(ev);
499
500 // Bubbles are always interested in all touch events!
501 return true;
502 }
503
504 @Override
505 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
506 float viewInitialY, float dx, float dy) {
507 // If we're expanding or collapsing, ignore all touch events.
508 if (mIsExpansionAnimating) {
509 return;
510 }
511
512 // Show the dismiss target, if we haven't already.
513 springInDismissTargetMaybe();
514
515 // First, see if the magnetized object consumes the event - if so, we shouldn't move the
516 // bubble since it's stuck to the target.
517 if (!passEventToMagnetizedObject(ev)) {
518 if (mBubbleData.isExpanded()) {
519 mExpandedAnimationController.dragBubbleOut(
520 v, viewInitialX + dx, viewInitialY + dy);
521 } else {
522 hideStackUserEducation(false /* fromExpansion */);
523 mStackAnimationController.moveStackFromTouch(
524 viewInitialX + dx, viewInitialY + dy);
525 }
526 }
527 }
528
529 @Override
530 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
531 float viewInitialY, float dx, float dy, float velX, float velY) {
532 // If we're expanding or collapsing, ignore all touch events.
533 if (mIsExpansionAnimating) {
534 return;
535 }
536
537 // First, see if the magnetized object consumes the event - if so, the bubble was
538 // released in the target or flung out of it, and we should ignore the event.
539 if (!passEventToMagnetizedObject(ev)) {
540 if (mBubbleData.isExpanded()) {
541 mExpandedAnimationController.snapBubbleBack(v, velX, velY);
542 } else {
543 // Fling the stack to the edge, and save whether or not it's going to end up on
544 // the left side of the screen.
545 mStackOnLeftOrWillBe =
546 mStackAnimationController.flingStackThenSpringToEdge(
547 viewInitialX + dx, velX, velY) <= 0;
548
549 updateBubbleZOrdersAndDotPosition(true /* animate */);
550
551 logBubbleEvent(null /* no bubble associated with bubble stack move */,
552 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
553 }
554
555 hideDismissTarget();
556 }
557 }
558 };
559
560 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
561 private OnClickListener mFlyoutClickListener = new OnClickListener() {
562 @Override
563 public void onClick(View view) {
564 if (maybeShowStackUserEducation()) {
565 // If we're showing user education, don't open the bubble show the education first
566 mBubbleToExpandAfterFlyoutCollapse = null;
567 } else {
568 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
569 }
570
571 mFlyout.removeCallbacks(mHideFlyout);
572 mHideFlyout.run();
573 }
574 };
575
576 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */
577 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() {
578
579 @Override
580 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
581 mFlyout.removeCallbacks(mHideFlyout);
582 return true;
583 }
584
585 @Override
586 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
587 float viewInitialY, float dx, float dy) {
588 setFlyoutStateForDragLength(dx);
589 }
590
591 @Override
592 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
593 float viewInitialY, float dx, float dy, float velX, float velY) {
594 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
595 final boolean metRequiredVelocity =
596 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
597 final boolean metRequiredDeltaX =
598 onLeft
599 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
600 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
601 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
602 final boolean shouldDismiss = metRequiredVelocity
603 || (metRequiredDeltaX && !isCancelFling);
604
605 mFlyout.removeCallbacks(mHideFlyout);
606 animateFlyoutCollapsed(shouldDismiss, velX);
607
608 maybeShowStackUserEducation();
609 }
610 };
611
Joshua Tsuji20103542020-02-18 14:06:28 -0500612 private ViewGroup mDismissTargetContainer;
613 private PhysicsAnimator<View> mDismissTargetAnimator;
614 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
615 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Issei Suzukic0387542019-03-08 17:31:14 +0100616
Lyn Hanf4730312019-06-18 11:18:58 -0700617 private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
Lyn Han3cd75d72020-02-15 19:10:12 -0800618
Joshua Tsujia2433db2020-03-12 17:56:22 -0400619 @Nullable
Lyn Han3cd75d72020-02-15 19:10:12 -0800620 private BubbleOverflow mBubbleOverflow;
Lyn Hanf4730312019-06-18 11:18:58 -0700621
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800622 private boolean mShouldShowUserEducation;
623 private boolean mAnimatingEducationAway;
624 private View mUserEducationView;
625
626 private boolean mShouldShowManageEducation;
627 private BubbleManageEducationView mManageEducationView;
628 private boolean mAnimatingManageEducationAway;
629
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400630 @SuppressLint("ClickableViewAccessibility")
Issei Suzukic0387542019-03-08 17:31:14 +0100631 public BubbleStackView(Context context, BubbleData data,
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500632 @Nullable SurfaceSynchronizer synchronizer,
Joshua Tsujibe60a582020-03-23 17:17:26 -0400633 FloatingContentCoordinator floatingContentCoordinator,
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400634 SysUiState sysUiState,
635 NotificationShadeWindowController notificationShadeWindowController) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800636 super(context);
637
Mady Mellorcfd06c12019-02-13 14:32:12 -0800638 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800639 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800640
Joshua Tsujibe60a582020-03-23 17:17:26 -0400641 mSysUiState = sysUiState;
Joshua Tsujiba9fef02020-04-09 17:40:35 -0400642 mNotificationShadeWindowController = notificationShadeWindowController;
Joshua Tsujibe60a582020-03-23 17:17:26 -0400643
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800644 Resources res = getResources();
Mady Mellor70958542019-09-24 17:12:46 -0700645 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800646 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellor70958542019-09-24 17:12:46 -0700647 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Lyn Han4a8efe32019-05-30 09:43:27 -0700648 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellore9371bc2019-07-10 18:50:59 -0700649 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800650 mExpandedAnimateXDistance =
651 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
652 mExpandedAnimateYDistance =
653 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700654 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
655
Joshua Tsujif44347f2019-02-12 14:28:06 -0500656 mStatusBarHeight =
657 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500658 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800659
660 mDisplaySize = new Point();
661 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700662 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700663 wm.getDefaultDisplay().getRealSize(mDisplaySize);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800664
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400665 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
666
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700667 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800668 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800669
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400670 mStackAnimationController = new StackAnimationController(
671 floatingContentCoordinator, this::getBubbleCount);
Lyn Hanf4730312019-06-18 11:18:58 -0700672
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700673 mExpandedAnimationController = new ExpandedAnimationController(
Lyn Hanf4730312019-06-18 11:18:58 -0700674 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
Issei Suzukic0387542019-03-08 17:31:14 +0100675 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800676
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800677 setUpUserEducation();
678
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800679 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400680 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800681 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800682 mBubbleContainer.setClipChildren(false);
683 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
684
Mady Mellor3dff9e62019-02-05 18:12:53 -0800685 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800686 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700687 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
688 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800689 mExpandedViewContainer.setClipChildren(false);
690 addView(mExpandedViewContainer);
691
Mady Mellor8bfe5412019-07-31 14:56:44 -0700692 setUpFlyout();
Joshua Tsuji6549e702019-05-02 13:13:16 -0400693 mFlyoutTransitionSpring.setSpring(new SpringForce()
Joshua Tsuji14e68552019-06-06 17:17:08 -0400694 .setStiffness(SpringForce.STIFFNESS_LOW)
Joshua Tsuji6549e702019-05-02 13:13:16 -0400695 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
696 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
697
Joshua Tsujie48c4112020-02-26 14:36:25 -0500698 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
Joshua Tsuji20103542020-02-18 14:06:28 -0500699 final View targetView = new DismissCircleView(context);
700 final FrameLayout.LayoutParams newParams =
Joshua Tsujie48c4112020-02-26 14:36:25 -0500701 new FrameLayout.LayoutParams(targetSize, targetSize);
Joshua Tsuji20103542020-02-18 14:06:28 -0500702 newParams.gravity = Gravity.CENTER;
703 targetView.setLayoutParams(newParams);
704 mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
705
706 mDismissTargetContainer = new FrameLayout(context);
707 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
Joshua Tsuji6549e702019-05-02 13:13:16 -0400708 MATCH_PARENT,
Joshua Tsujif39539d2020-04-03 18:53:06 -0400709 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
Joshua Tsuji6549e702019-05-02 13:13:16 -0400710 Gravity.BOTTOM));
Joshua Tsuji20103542020-02-18 14:06:28 -0500711 mDismissTargetContainer.setClipChildren(false);
712 mDismissTargetContainer.addView(targetView);
713 mDismissTargetContainer.setVisibility(View.INVISIBLE);
714 addView(mDismissTargetContainer);
715
716 // Start translated down so the target springs up.
717 targetView.setTranslationY(
Joshua Tsujif39539d2020-04-03 18:53:06 -0400718 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height));
Joshua Tsuji20103542020-02-18 14:06:28 -0500719
720 // Save the MagneticTarget instance for the newly set up view - we'll add this to the
721 // MagnetizedObjects.
722 mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400723
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800724 mExpandedViewXAnim =
725 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
726 mExpandedViewXAnim.setSpring(
727 new SpringForce()
728 .setStiffness(SpringForce.STIFFNESS_LOW)
729 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
730
731 mExpandedViewYAnim =
732 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
733 mExpandedViewYAnim.setSpring(
734 new SpringForce()
735 .setStiffness(SpringForce.STIFFNESS_LOW)
736 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorbc078c22019-03-26 17:10:34 -0700737 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700738 if (mIsExpanded && mExpandedBubble != null
739 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -0800740 mExpandedBubble.getExpandedView().updateView();
Mady Mellorbc078c22019-03-26 17:10:34 -0700741 }
742 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800743
744 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800745 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500746 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800747
Lyn Hancd4f87e2020-02-19 20:33:45 -0800748 setUpOverflow();
Lyn Hanb58c7562020-01-07 14:29:20 -0800749
Mady Mellor5d8f1402019-02-21 18:23:52 -0800750 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
Mady Mellorbc078c22019-03-26 17:10:34 -0700751 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800752 return view.onApplyWindowInsets(insets);
753 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800754 mExpandedAnimationController.updateYPosition(
755 // Update the insets after we're done translating otherwise position
756 // calculation for them won't be correct.
Lyn Hanb58c7562020-01-07 14:29:20 -0800757 () -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700758 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800759 mExpandedBubble.getExpandedView().updateInsets(insets);
760 }
761 });
Mady Mellor5d8f1402019-02-21 18:23:52 -0800762 return view.onApplyWindowInsets(insets);
763 });
Mark Renouf821e6782019-04-01 14:17:37 -0400764
Lyn Hanf4730312019-06-18 11:18:58 -0700765 mOrientationChangedListener =
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400766 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
Mady Mellor9be3bed2019-08-21 17:26:26 -0700767 mExpandedAnimationController.updateOrientation(mOrientation, mDisplaySize);
768 mStackAnimationController.updateOrientation(mOrientation);
769
770 // Reposition & adjust the height for new orientation
771 if (mIsExpanded) {
772 mExpandedViewContainer.setTranslationY(getExpandedViewY());
Mady Melloradd5c6a92020-03-31 17:22:48 -0700773 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800774 mExpandedBubble.getExpandedView().updateView();
775 }
Mady Mellor9be3bed2019-08-21 17:26:26 -0700776 }
777
778 // Need to update the padding around the view
779 WindowInsets insets = getRootWindowInsets();
780 int leftPadding = mExpandedViewPadding;
781 int rightPadding = mExpandedViewPadding;
782 if (insets != null) {
783 // Can't have the expanded view overlaying notches
784 int cutoutLeft = 0;
785 int cutoutRight = 0;
786 DisplayCutout cutout = insets.getDisplayCutout();
787 if (cutout != null) {
788 cutoutLeft = cutout.getSafeInsetLeft();
789 cutoutRight = cutout.getSafeInsetRight();
790 }
791 // Or overlaying nav or status bar
792 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
793 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
794 }
795 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
796 rightPadding, mExpandedViewPadding);
797
Lyn Hanf4730312019-06-18 11:18:58 -0700798 if (mIsExpanded) {
799 // Re-draw bubble row and pointer for new orientation.
800 mExpandedAnimationController.expandFromStack(() -> {
801 updatePointerPosition();
802 } /* after */);
803 }
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400804 if (mVerticalPosPercentBeforeRotation >= 0) {
805 mStackAnimationController.moveStackToSimilarPositionAfterRotation(
806 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
807 }
Lyn Hanf4730312019-06-18 11:18:58 -0700808 removeOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400809 };
810
Mark Renouf821e6782019-04-01 14:17:37 -0400811 // This must be a separate OnDrawListener since it should be called for every draw.
812 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400813
814 final ColorMatrix animatedMatrix = new ColorMatrix();
815 final ColorMatrix darkenMatrix = new ColorMatrix();
816
817 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
818 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
819 final float animatedValue = (float) animation.getAnimatedValue();
820 animatedMatrix.setSaturation(animatedValue);
821
822 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
823 darkenMatrix.setScale(
824 1f - animatedDarkenValue /* red */,
825 1f - animatedDarkenValue /* green */,
826 1f - animatedDarkenValue /* blue */,
827 1f /* alpha */);
828
829 // Concat the matrices so that the animatedMatrix both desaturates and darkens.
830 animatedMatrix.postConcat(darkenMatrix);
831
832 // Update the paint and apply it to the bubble container.
833 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
834 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
835 });
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400836
837 // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts,
838 // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
839 setOnTouchListener((view, ev) -> {
840 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
841 if (mBubbleData.isExpanded()) {
842 mBubbleData.setExpanded(false);
843 }
844 }
845
846 return false;
847 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800848 }
849
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800850 private void setUpUserEducation() {
851 if (mUserEducationView != null) {
852 removeView(mUserEducationView);
853 }
854 mShouldShowUserEducation = shouldShowBubblesEducation();
855 if (DEBUG_USER_EDUCATION) {
856 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
857 }
858 if (mShouldShowUserEducation) {
859 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
860 false /* attachToRoot */);
861 mUserEducationView.setVisibility(GONE);
862
863 final TypedArray ta = mContext.obtainStyledAttributes(
864 new int[] {android.R.attr.colorAccent,
865 android.R.attr.textColorPrimaryInverse});
866 final int bgColor = ta.getColor(0, Color.BLACK);
867 int textColor = ta.getColor(1, Color.WHITE);
868 ta.recycle();
869 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
870
871 TextView title = mUserEducationView.findViewById(R.id.user_education_title);
872 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
873 title.setTextColor(textColor);
874 description.setTextColor(textColor);
875
876 addView(mUserEducationView);
877 }
878
879 if (mManageEducationView != null) {
880 removeView(mManageEducationView);
881 }
882 mShouldShowManageEducation = shouldShowManageEducation();
883 if (DEBUG_USER_EDUCATION) {
884 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
885 }
886 if (mShouldShowManageEducation) {
887 mManageEducationView = (BubbleManageEducationView)
888 mInflater.inflate(R.layout.bubbles_manage_button_education, this,
889 false /* attachToRoot */);
890 mManageEducationView.setVisibility(GONE);
891 mManageEducationView.setElevation(mBubbleElevation);
892
893 addView(mManageEducationView);
Lyn Hanb58c7562020-01-07 14:29:20 -0800894 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800895 }
896
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400897 @SuppressLint("ClickableViewAccessibility")
Mady Mellor8bfe5412019-07-31 14:56:44 -0700898 private void setUpFlyout() {
899 if (mFlyout != null) {
900 removeView(mFlyout);
901 }
902 mFlyout = new BubbleFlyoutView(getContext());
903 mFlyout.setVisibility(GONE);
904 mFlyout.animate()
905 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
906 .setInterpolator(new AccelerateDecelerateInterpolator());
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400907 mFlyout.setOnClickListener(mFlyoutClickListener);
908 mFlyout.setOnTouchListener(mFlyoutTouchListener);
Mady Mellor8bfe5412019-07-31 14:56:44 -0700909 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
910 }
911
Lyn Hancd4f87e2020-02-19 20:33:45 -0800912 private void setUpOverflow() {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800913 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
914 return;
915 }
Lyn Hancd4f87e2020-02-19 20:33:45 -0800916 int overflowBtnIndex = 0;
917 if (mBubbleOverflow == null) {
Mady Mellor0122cc92020-02-27 12:15:39 -0800918 mBubbleOverflow = new BubbleOverflow(getContext());
919 mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800920 } else {
921 mBubbleContainer.removeView(mBubbleOverflow.getBtn());
922 mBubbleOverflow.updateIcon(mContext, this);
Lyn Hane1cf3b22020-04-01 13:39:43 -0700923 overflowBtnIndex = mBubbleContainer.getChildCount();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800924 }
925 mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
926 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
927
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400928 mBubbleOverflow.getBtn().setOnClickListener(v -> setSelectedBubble(mBubbleOverflow));
Lyn Hancd4f87e2020-02-19 20:33:45 -0800929 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700930 /**
Lyn Han02cca812019-04-02 16:27:32 -0700931 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700932 */
Lyn Han02cca812019-04-02 16:27:32 -0700933 public void onThemeChanged() {
Mady Mellor8bfe5412019-07-31 14:56:44 -0700934 setUpFlyout();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800935 setUpOverflow();
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800936 setUpUserEducation();
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700937 }
938
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400939 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
Lyn Hanf4730312019-06-18 11:18:58 -0700940 public void onOrientationChanged(int orientation) {
941 mOrientation = orientation;
942
Mady Mellore19353d2019-08-21 17:25:02 -0700943 // Display size is based on the rotation device was in when requested, we should update it
Mady Mellor9be3bed2019-08-21 17:26:26 -0700944 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700945 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
946 wm.getDefaultDisplay().getRealSize(mDisplaySize);
947
Mady Mellor818eef02019-08-16 16:12:29 -0700948 // Some resources change depending on orientation
949 Resources res = getContext().getResources();
950 mStatusBarHeight = res.getDimensionPixelSize(
951 com.android.internal.R.dimen.status_bar_height);
952 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
953
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400954 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
955 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
956 mVerticalPosPercentBeforeRotation =
957 (mStackAnimationController.getStackPosition().y - allowablePos.top)
958 / (allowablePos.bottom - allowablePos.top);
Joshua Tsujifaf890f2020-04-16 13:38:32 -0400959 mVerticalPosPercentBeforeRotation =
960 Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation));
Lyn Hanf4730312019-06-18 11:18:58 -0700961 addOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400962 hideFlyoutImmediate();
963 }
964
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800965 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800966 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
967 getBoundsOnScreen(outRect);
968 }
969
970 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800971 protected void onDetachedFromWindow() {
972 super.onDetachedFromWindow();
973 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
974 }
975
976 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800977 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
978 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Hane68d0912019-05-02 18:28:01 -0700979
980 // Custom actions.
981 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
982 getContext().getResources()
983 .getString(R.string.bubble_accessibility_action_move_top_left));
984 info.addAction(moveTopLeft);
985
986 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
987 getContext().getResources()
988 .getString(R.string.bubble_accessibility_action_move_top_right));
989 info.addAction(moveTopRight);
990
991 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
992 getContext().getResources()
993 .getString(R.string.bubble_accessibility_action_move_bottom_left));
994 info.addAction(moveBottomLeft);
995
996 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
997 getContext().getResources()
998 .getString(R.string.bubble_accessibility_action_move_bottom_right));
999 info.addAction(moveBottomRight);
1000
1001 // Default actions.
1002 info.addAction(AccessibilityAction.ACTION_DISMISS);
Mady Mellor217b2e92019-02-27 11:44:16 -08001003 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -07001004 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -08001005 } else {
Lyn Hane68d0912019-05-02 18:28:01 -07001006 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -08001007 }
1008 }
1009
1010 @Override
1011 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1012 if (super.performAccessibilityActionInternal(action, arguments)) {
1013 return true;
1014 }
Lyn Hane68d0912019-05-02 18:28:01 -07001015 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
1016
1017 // R constants are not final so we cannot use switch-case here.
1018 if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
1019 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
1020 return true;
1021 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
1022 mBubbleData.setExpanded(false);
1023 return true;
1024 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
1025 mBubbleData.setExpanded(true);
1026 return true;
1027 } else if (action == R.id.action_move_top_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001028 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001029 return true;
1030 } else if (action == R.id.action_move_top_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001031 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001032 return true;
1033 } else if (action == R.id.action_move_bottom_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001034 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001035 return true;
1036 } else if (action == R.id.action_move_bottom_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001037 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001038 return true;
Mady Mellor217b2e92019-02-27 11:44:16 -08001039 }
1040 return false;
1041 }
1042
Lyn Han6c40fe72019-05-08 14:06:33 -07001043 /**
1044 * Update content description for a11y TalkBack.
1045 */
1046 public void updateContentDescription() {
1047 if (mBubbleData.getBubbles().isEmpty()) {
1048 return;
1049 }
1050 Bubble topBubble = mBubbleData.getBubbles().get(0);
1051 String appName = topBubble.getAppName();
Ned Burns00b4b2d2019-10-17 22:09:27 -04001052 Notification notification = topBubble.getEntry().getSbn().getNotification();
Lyn Han6c40fe72019-05-08 14:06:33 -07001053 CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
Lyn Han93cd3132020-02-18 18:22:05 -08001054 String titleStr = getResources().getString(R.string.notification_bubble_title);
Lyn Han6c40fe72019-05-08 14:06:33 -07001055 if (titleCharSeq != null) {
1056 titleStr = titleCharSeq.toString();
1057 }
1058 int moreCount = mBubbleContainer.getChildCount() - 1;
1059
1060 // Example: Title from app name.
1061 String singleDescription = getResources().getString(
1062 R.string.bubble_content_description_single, titleStr, appName);
1063
1064 // Example: Title from app name and 4 more.
1065 String stackDescription = getResources().getString(
1066 R.string.bubble_content_description_stack, titleStr, appName, moreCount);
1067
1068 if (mIsExpanded) {
1069 // TODO(b/129522932) - update content description for each bubble in expanded view.
1070 } else {
1071 // Collapsed stack.
1072 if (moreCount > 0) {
1073 mBubbleContainer.setContentDescription(stackDescription);
1074 } else {
1075 mBubbleContainer.setContentDescription(singleDescription);
1076 }
1077 }
1078 }
1079
Mark Renouf821e6782019-04-01 14:17:37 -04001080 private void updateSystemGestureExcludeRects() {
1081 // Exclude the region occupied by the first BubbleView in the stack
1082 Rect excludeZone = mSystemGestureExclusionRects.get(0);
Lyn Hanc47e1712020-01-28 21:43:34 -08001083 if (getBubbleCount() > 0) {
Mark Renouf821e6782019-04-01 14:17:37 -04001084 View firstBubble = mBubbleContainer.getChildAt(0);
1085 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
1086 firstBubble.getBottom());
1087 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
1088 (int) (firstBubble.getTranslationY() + 0.5f));
1089 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
1090 } else {
1091 excludeZone.setEmpty();
1092 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
1093 }
1094 }
1095
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001096 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -08001097 * Sets the listener to notify when the bubble stack is expanded.
1098 */
1099 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
1100 mExpandListener = listener;
1101 }
1102
1103 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001104 * Whether the stack of bubbles is expanded or not.
1105 */
1106 public boolean isExpanded() {
1107 return mIsExpanded;
1108 }
1109
1110 /**
Mady Mellor047e24e2019-08-05 11:35:40 -07001111 * Whether the stack of bubbles is animating to or from expansion.
1112 */
1113 public boolean isExpansionAnimating() {
1114 return mIsExpansionAnimating;
1115 }
1116
1117 /**
Mady Mellorb8aaf972019-11-26 10:28:00 -08001118 * The {@link BadgedImageView} that is expanded, null if one does not exist.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001119 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001120 View getExpandedBubbleView() {
Mady Mellored99c272019-06-13 15:58:30 -07001121 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
Mady Mellor3dff9e62019-02-05 18:12:53 -08001122 }
1123
1124 /**
1125 * The {@link Bubble} that is expanded, null if one does not exist.
1126 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001127 @Nullable
Lyn Han9f66c3b2020-03-05 23:59:29 -08001128 BubbleViewProvider getExpandedBubble() {
1129 return mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001130 }
1131
Mark Renouf71a3af62019-04-08 15:02:54 -04001132 // via BubbleData.Listener
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001133 @SuppressLint("ClickableViewAccessibility")
Mark Renouf71a3af62019-04-08 15:02:54 -04001134 void addBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001135 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001136 Log.d(TAG, "addBubble: " + bubble);
1137 }
Joshua Tsujib35f5912019-07-24 16:15:21 -04001138
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001139 if (getBubbleCount() == 0 && mShouldShowUserEducation) {
1140 // Override the default stack position if we're showing user education.
1141 mStackAnimationController.setStackPosition(
1142 mStackAnimationController.getDefaultStartPosition());
1143 }
1144
Lyn Hanc47e1712020-01-28 21:43:34 -08001145 if (getBubbleCount() == 0) {
Joshua Tsujib35f5912019-07-24 16:15:21 -04001146 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
1147 }
1148
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001149 if (bubble.getIconView() == null) {
1150 return;
1151 }
1152
Joshua Tsujib35f5912019-07-24 16:15:21 -04001153 // Set the dot position to the opposite of the side the stack is resting on, since the stack
1154 // resting slightly off-screen would result in the dot also being off-screen.
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001155 bubble.getIconView().setDotPositionOnLeft(
Joshua Tsujib35f5912019-07-24 16:15:21 -04001156 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
1157
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001158 bubble.getIconView().setOnClickListener(mBubbleClickListener);
1159 bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
1160
Mady Mellored99c272019-06-13 15:58:30 -07001161 mBubbleContainer.addView(bubble.getIconView(), 0,
Mark Renouf71a3af62019-04-08 15:02:54 -04001162 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellored99c272019-06-13 15:58:30 -07001163 ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
Mark Renoufba5ab512019-05-02 15:21:01 -04001164 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001165 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001166 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001167 }
1168
1169 // via BubbleData.Listener
1170 void removeBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001171 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001172 Log.d(TAG, "removeBubble: " + bubble);
1173 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001174 // Remove it from the views
Lyn Hane1395572020-03-23 15:48:54 -07001175 for (int i = 0; i < getBubbleCount(); i++) {
1176 View v = mBubbleContainer.getChildAt(i);
1177 if (v instanceof BadgedImageView
1178 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) {
1179 mBubbleContainer.removeViewAt(i);
Mady Mellore967e962020-03-26 17:36:44 -07001180 bubble.cleanupViews();
Lyn Hane1395572020-03-23 15:48:54 -07001181 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
1182 return;
1183 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001184 }
Lyn Hane1395572020-03-23 15:48:54 -07001185 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
Lyn Hanb58c7562020-01-07 14:29:20 -08001186 }
1187
1188 private void updateOverflowBtnVisibility(boolean apply) {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001189 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1190 return;
1191 }
Lyn Hanb58c7562020-01-07 14:29:20 -08001192 if (mIsExpanded) {
1193 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001194 Log.d(TAG, "Show overflow button.");
Lyn Hanb58c7562020-01-07 14:29:20 -08001195 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001196 mBubbleOverflow.setBtnVisible(VISIBLE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001197 if (apply) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001198 mExpandedAnimationController.expandFromStack(() -> {
1199 updatePointerPosition();
1200 } /* after */);
Lyn Hanb58c7562020-01-07 14:29:20 -08001201 }
1202 } else {
1203 if (DEBUG_BUBBLE_STACK_VIEW) {
1204 Log.d(TAG, "Collapsed. Hide overflow button.");
1205 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001206 mBubbleOverflow.setBtnVisible(GONE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001207 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001208 }
1209
1210 // via BubbleData.Listener
1211 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -04001212 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001213 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001214 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001215 }
1216
Mark Renoufba5ab512019-05-02 15:21:01 -04001217 public void updateBubbleOrder(List<Bubble> bubbles) {
1218 for (int i = 0; i < bubbles.size(); i++) {
1219 Bubble bubble = bubbles.get(i);
Mady Mellored99c272019-06-13 15:58:30 -07001220 mBubbleContainer.reorderView(bubble.getIconView(), i);
Mark Renoufba5ab512019-05-02 15:21:01 -04001221 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001222 updateBubbleZOrdersAndDotPosition(false /* animate */);
Lyn Hanf44562b2020-03-30 16:40:46 -07001223 updatePointerPosition();
Mark Renoufba5ab512019-05-02 15:21:01 -04001224 }
1225
Mady Melloredd4ee12019-01-18 10:45:11 -08001226 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001227 * Changes the currently selected bubble. If the stack is already expanded, the newly selected
1228 * bubble will be shown immediately. This does not change the expanded state or change the
1229 * position of any bubble.
1230 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001231 // via BubbleData.Listener
Lyn Han3cd75d72020-02-15 19:10:12 -08001232 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001233 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001234 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
1235 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001236 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
1237 return;
1238 }
Lyn Han89fb39d2020-04-07 11:51:07 -07001239 if (bubbleToSelect == null || bubbleToSelect.getKey() != BubbleOverflow.KEY) {
1240 mBubbleData.setShowingOverflow(false);
1241 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001242 final BubbleViewProvider previouslySelected = mExpandedBubble;
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001243 mExpandedBubble = bubbleToSelect;
Lyn Han89fb39d2020-04-07 11:51:07 -07001244 updatePointerPosition();
Issei Suzukicac2a502019-04-16 16:52:50 +02001245
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001246 if (mIsExpanded) {
1247 // Make the container of the expanded view transparent before removing the expanded view
1248 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
1249 // expanded view becomes visible on the screen. See b/126856255
1250 mExpandedViewContainer.setAlpha(0.0f);
1251 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Lyn Han3cd75d72020-02-15 19:10:12 -08001252 previouslySelected.setContentVisibility(false);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001253 updateExpandedBubble();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001254 requestUpdate();
Lyn Han3cd75d72020-02-15 19:10:12 -08001255
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001256 logBubbleEvent(previouslySelected,
1257 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
1258 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor99a302602019-06-14 11:39:56 -07001259 notifyExpansionChanged(previouslySelected, false /* expanded */);
1260 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001261 });
1262 }
1263 }
1264
1265 /**
1266 * Changes the expanded state of the stack.
1267 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001268 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001269 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001270 // via BubbleData.Listener
1271 public void setExpanded(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001272 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001273 Log.d(TAG, "setExpanded: " + shouldExpand);
1274 }
Lyn Han285ad302019-05-29 19:01:39 -07001275 if (shouldExpand == mIsExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001276 return;
1277 }
Joshua Tsujibe60a582020-03-23 17:17:26 -04001278
1279 mSysUiState
1280 .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
1281 .commitUpdate(mContext.getDisplayId());
1282
Lyn Han285ad302019-05-29 19:01:39 -07001283 if (mIsExpanded) {
1284 animateCollapse();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001285 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001286 } else {
Lyn Han285ad302019-05-29 19:01:39 -07001287 animateExpansion();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001288 // TODO: move next line to BubbleData
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001289 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
1290 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001291 }
Mady Mellor99a302602019-06-14 11:39:56 -07001292 notifyExpansionChanged(mExpandedBubble, mIsExpanded);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001293 }
1294
1295 /**
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001296 * If necessary, shows the user education view for the bubble stack. This appears the first
1297 * time a user taps on a bubble.
1298 *
1299 * @return true if user education was shown, false otherwise.
1300 */
1301 private boolean maybeShowStackUserEducation() {
1302 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001303 mUserEducationView.setAlpha(0);
1304 mUserEducationView.setVisibility(VISIBLE);
1305 // Post so we have height of mUserEducationView
1306 mUserEducationView.post(() -> {
1307 final int viewHeight = mUserEducationView.getHeight();
1308 PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
1309 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
1310 mUserEducationView.setTranslationY(translationY);
1311 mUserEducationView.animate()
1312 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1313 .setInterpolator(FAST_OUT_SLOW_IN)
1314 .alpha(1);
1315 });
1316 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
1317 return true;
1318 }
1319 return false;
1320 }
1321
1322 /**
1323 * If necessary, hides the user education view for the bubble stack.
1324 *
1325 * @param fromExpansion if true this indicates the hide is happening due to the bubble being
1326 * expanded, false if due to a touch outside of the bubble stack.
1327 */
1328 void hideStackUserEducation(boolean fromExpansion) {
1329 if (mShouldShowUserEducation
1330 && mUserEducationView.getVisibility() == VISIBLE
1331 && !mAnimatingEducationAway) {
1332 mAnimatingEducationAway = true;
1333 mUserEducationView.animate()
1334 .alpha(0)
1335 .setDuration(fromExpansion
1336 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1337 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1338 .withEndAction(() -> {
1339 mAnimatingEducationAway = false;
1340 mShouldShowUserEducation = shouldShowBubblesEducation();
1341 mUserEducationView.setVisibility(GONE);
1342 });
1343 }
1344 }
1345
1346 /**
1347 * If necessary, toggles the user education view for the manage button. This is shown when the
1348 * bubble stack is expanded for the first time.
1349 *
1350 * @param show whether the user education view should show or not.
1351 */
1352 void maybeShowManageEducation(boolean show) {
1353 if (mManageEducationView == null) {
1354 return;
1355 }
1356 if (show
1357 && mShouldShowManageEducation
1358 && mManageEducationView.getVisibility() != VISIBLE
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001359 && mIsExpanded
1360 && mExpandedBubble.getExpandedView() != null) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001361 mManageEducationView.setAlpha(0);
1362 mManageEducationView.setVisibility(VISIBLE);
1363 mManageEducationView.post(() -> {
1364 final Rect position =
1365 mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
1366 final int viewHeight = mManageEducationView.getManageViewHeight();
1367 final int inset = getResources().getDimensionPixelSize(
1368 R.dimen.bubbles_manage_education_top_inset);
1369 mManageEducationView.bringToFront();
1370 mManageEducationView.setManageViewPosition(position.left,
1371 position.top - viewHeight + inset);
1372 mManageEducationView.setPointerPosition(position.centerX() - position.left);
1373 mManageEducationView.animate()
1374 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1375 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
1376 });
1377 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
1378 } else if (!show
1379 && mManageEducationView.getVisibility() == VISIBLE
1380 && !mAnimatingManageEducationAway) {
1381 mManageEducationView.animate()
1382 .alpha(0)
1383 .setDuration(mIsExpansionAnimating
1384 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1385 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1386 .withEndAction(() -> {
1387 mAnimatingManageEducationAway = false;
1388 mShouldShowManageEducation = shouldShowManageEducation();
1389 mManageEducationView.setVisibility(GONE);
1390 });
1391 }
1392 }
1393
1394 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001395 * Dismiss the stack of bubbles.
Lyn Han1b4f25e2019-06-11 13:56:34 -07001396 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001397 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001398 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001399 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -07001400 void stackDismissed(int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001401 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001402 Log.d(TAG, "stackDismissed: reason=" + reason);
1403 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001404 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -05001405 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001406 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001407 }
1408
1409 /**
Joshua Tsujif5c6a9c2020-02-25 17:47:59 -05001410 * @deprecated use {@link #setExpanded(boolean)} and
1411 * {@link BubbleData#setSelectedBubble(Bubble)}
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001412 */
1413 @Deprecated
1414 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -08001415 void collapseStack(Runnable endRunnable) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001416 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001417 Log.d(TAG, "collapseStack(endRunnable)");
1418 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001419 mBubbleData.setExpanded(false);
Mady Mellor9801e852019-01-22 14:50:28 -08001420 // TODO - use the runnable at end of animation
1421 endRunnable.run();
1422 }
1423
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001424 void showExpandedViewContents(int displayId) {
1425 if (mExpandedBubble != null
Mady Melloradd5c6a92020-03-31 17:22:48 -07001426 && mExpandedBubble.getExpandedView() != null
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001427 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
1428 mExpandedBubble.setContentVisibility(true);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001429 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001430 }
1431
Lyn Han285ad302019-05-29 19:01:39 -07001432 private void beforeExpandedViewAnimation() {
1433 hideFlyoutImmediate();
1434 updateExpandedBubble();
1435 updateExpandedView();
1436 mIsExpansionAnimating = true;
1437 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001438
Lyn Han285ad302019-05-29 19:01:39 -07001439 private void afterExpandedViewAnimation() {
1440 updateExpandedView();
1441 mIsExpansionAnimating = false;
1442 requestUpdate();
1443 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001444
Lyn Han285ad302019-05-29 19:01:39 -07001445 private void animateCollapse() {
1446 mIsExpanded = false;
Lyn Han3cd75d72020-02-15 19:10:12 -08001447 final BubbleViewProvider previouslySelected = mExpandedBubble;
Lyn Han285ad302019-05-29 19:01:39 -07001448 beforeExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001449 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001450
Lyn Hanb58c7562020-01-07 14:29:20 -08001451 if (DEBUG_BUBBLE_STACK_VIEW) {
1452 Log.d(TAG, "animateCollapse");
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001453 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
Lyn Han9f66c3b2020-03-05 23:59:29 -08001454 mExpandedBubble));
Lyn Hanb58c7562020-01-07 14:29:20 -08001455 }
1456 updateOverflowBtnVisibility(/* apply */ false);
Lyn Han285ad302019-05-29 19:01:39 -07001457 mBubbleContainer.cancelAllAnimations();
1458 mExpandedAnimationController.collapseBackToStack(
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001459 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
1460 /* collapseTo */,
Lyn Han285ad302019-05-29 19:01:39 -07001461 () -> {
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001462 mBubbleContainer.setActiveController(mStackAnimationController);
Lyn Han285ad302019-05-29 19:01:39 -07001463 afterExpandedViewAnimation();
Lyn Han3cd75d72020-02-15 19:10:12 -08001464 previouslySelected.setContentVisibility(false);
Lyn Han285ad302019-05-29 19:01:39 -07001465 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001466
Lyn Han285ad302019-05-29 19:01:39 -07001467 mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
1468 mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
1469 mExpandedViewContainer.animate()
1470 .setDuration(100)
1471 .alpha(0f);
1472 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001473
Lyn Han285ad302019-05-29 19:01:39 -07001474 private void animateExpansion() {
1475 mIsExpanded = true;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001476 hideStackUserEducation(true /* fromExpansion */);
Lyn Han285ad302019-05-29 19:01:39 -07001477 beforeExpandedViewAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001478
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001479 mBubbleContainer.setActiveController(mExpandedAnimationController);
Lyn Hanb58c7562020-01-07 14:29:20 -08001480 updateOverflowBtnVisibility(/* apply */ false);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001481 mExpandedAnimationController.expandFromStack(() -> {
1482 updatePointerPosition();
1483 afterExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001484 maybeShowManageEducation(true);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001485 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001486
Lyn Han285ad302019-05-29 19:01:39 -07001487 mExpandedViewContainer.setTranslationX(getCollapsedX());
1488 mExpandedViewContainer.setTranslationY(getCollapsedY());
1489 mExpandedViewContainer.setAlpha(0f);
1490
1491 mExpandedViewXAnim.animateToFinalPosition(0f);
1492 mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1493 mExpandedViewContainer.animate()
1494 .setDuration(100)
1495 .alpha(1f);
1496 }
1497
1498 private float getCollapsedX() {
1499 return mStackAnimationController.getStackPosition().x < getWidth() / 2
1500 ? -mExpandedAnimateXDistance
1501 : mExpandedAnimateXDistance;
1502 }
1503
1504 private float getCollapsedY() {
1505 return Math.min(mStackAnimationController.getStackPosition().y,
1506 mExpandedAnimateYDistance);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001507 }
1508
Lyn Han3cd75d72020-02-15 19:10:12 -08001509 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
Mady Mellor99a302602019-06-14 11:39:56 -07001510 if (mExpandListener != null && bubble != null) {
1511 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001512 }
1513 }
1514
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001515 /** Return the BubbleView at the given index from the bubble container. */
Mady Mellorb8aaf972019-11-26 10:28:00 -08001516 public BadgedImageView getBubbleAt(int i) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001517 return getBubbleCount() > i
Mady Mellorb8aaf972019-11-26 10:28:00 -08001518 ? (BadgedImageView) mBubbleContainer.getChildAt(i)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001519 : null;
1520 }
1521
Joshua Tsujia19515f2019-02-13 18:02:29 -05001522 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
1523 public void onImeVisibilityChanged(boolean visible, int height) {
Mady Mellordf611cf2019-08-21 17:28:49 -07001524 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
Joshua Tsuji4b395912019-04-19 17:18:40 -04001525
Joshua Tsujiff6b0f22020-03-09 14:55:19 -04001526 if (!mIsExpanded && getBubbleCount() > 0) {
1527 final float stackDestinationY =
1528 mStackAnimationController.animateForImeVisibility(visible);
1529
1530 // How far the stack is animating due to IME, we'll just animate the flyout by that
1531 // much too.
1532 final float stackDy =
1533 stackDestinationY - mStackAnimationController.getStackPosition().y;
1534
1535 // If the flyout is visible, translate it along with the bubble stack.
1536 if (mFlyout.getVisibility() == VISIBLE) {
1537 PhysicsAnimator.getInstance(mFlyout)
1538 .spring(DynamicAnimation.TRANSLATION_Y,
1539 mFlyout.getTranslationY() + stackDy,
1540 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
1541 .start();
1542 }
Joshua Tsujia19515f2019-02-13 18:02:29 -05001543 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001544 }
1545
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001546 /**
1547 * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a
1548 * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV).
1549 * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided
1550 * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to
1551 * the special nature of ActivityView, it does not respect the standard
1552 * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for
1553 * this purpose.
1554 *
1555 * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation
1556 * properties for performance reasons. This means that the default implementation of this method
1557 * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in
1558 * it not receiving any touch events. This was previously addressed by returning false in the
1559 * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any
1560 * touch handlers in the stack or its child views.
1561 *
1562 * To support touch handlers, we're overriding this method to leave the ActivityView's touchable
1563 * region alone. The only touchable part of the stack that can ever overlap the AV is a
1564 * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually
1565 * updating the touchable region to allow users to grab a bubble while it completes its ~50ms
1566 * animation back to the bubble row.
1567 *
1568 * NOTE: Any future additions to the stack that obscure the ActivityView region will need their
1569 * bounds subtracted here in order to receive touch events.
1570 */
1571 @Override
1572 public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
Joshua Tsujiba9fef02020-04-09 17:40:35 -04001573 // If the notification shade is expanded, we shouldn't let the ActivityView steal any touch
1574 // events from any location.
1575 if (mNotificationShadeWindowController.getPanelExpanded()) {
1576 touchableRegion.setEmpty();
1577 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001578 }
1579
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001580 /**
1581 * If you're here because you're not receiving touch events on a view that is a descendant of
1582 * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the
1583 * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView
1584 * consumes all touch events within its bounds, even for views like the BubbleStackView that are
1585 * above it. It ignores typical view touch handling methods like this one and
1586 * dispatchTouchEvent.
1587 */
1588 @Override
1589 public boolean onInterceptTouchEvent(MotionEvent ev) {
1590 return super.onInterceptTouchEvent(ev);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001591 }
1592
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001593 @Override
1594 public boolean dispatchTouchEvent(MotionEvent ev) {
1595 boolean dispatched = super.dispatchTouchEvent(ev);
1596
1597 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned
1598 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will
1599 // then be passed to the new bubble, which will not consume them since it hasn't received an
1600 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler
1601 // until the current gesture ends with an ACTION_UP event.
1602 if (!dispatched && !mIsExpanded && mIsGestureInProgress) {
1603 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev);
Joshua Tsuji442b6272019-02-08 13:23:43 -05001604 }
1605
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001606 mIsGestureInProgress =
1607 ev.getAction() != MotionEvent.ACTION_UP
1608 && ev.getAction() != MotionEvent.ACTION_CANCEL;
1609
1610 return dispatched;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001611 }
1612
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001613 void setFlyoutStateForDragLength(float deltaX) {
Joshua Tsuji8e05aab2019-08-22 14:57:50 -04001614 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1615 // is continually called.
1616 if (mFlyout.getWidth() <= 0) {
1617 return;
1618 }
1619
Joshua Tsuji6549e702019-05-02 13:13:16 -04001620 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1621 mFlyoutDragDeltaX = deltaX;
1622
1623 final float collapsePercent =
1624 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1625 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1626
Lyn Han61d5d562019-07-01 17:39:38 -07001627 // Calculate how to translate the flyout if it has been dragged too far in either direction.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001628 float overscrollTranslation = 0f;
1629 if (collapsePercent < 0f || collapsePercent > 1f) {
1630 // Whether we are more than 100% transitioned to the dot.
1631 final boolean overscrollingPastDot = collapsePercent > 1f;
1632
1633 // Whether we are overscrolling physically to the left - this can either be pulling the
1634 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1635 // after it has already become the dot.
1636 final boolean overscrollingLeft =
1637 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001638 overscrollTranslation =
1639 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1640 * (overscrollingLeft ? -1 : 1)
1641 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001642 // Attenuate the smaller dot less than the larger flyout.
1643 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001644 }
1645
1646 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1647 }
1648
Joshua Tsuji20103542020-02-18 14:06:28 -05001649 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001650 private boolean passEventToMagnetizedObject(MotionEvent event) {
Joshua Tsuji20103542020-02-18 14:06:28 -05001651 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
1652 }
1653
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001654 /**
1655 * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the
1656 * stack, if we're collapsed.
1657 */
1658 private void dismissMagnetizedObject() {
1659 if (mIsExpanded) {
1660 final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
1661 final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView);
1662
1663 if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) {
1664 mBubbleData.notificationEntryRemoved(
1665 draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
1666 }
1667 } else {
1668 mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
1669 }
1670 }
1671
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001672 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1673 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1674 mDesaturateAndDarkenTargetView = targetView;
1675
1676 if (desaturateAndDarken) {
1677 // Use the animated paint for the bubbles.
1678 mDesaturateAndDarkenTargetView.setLayerType(
1679 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1680 mDesaturateAndDarkenAnimator.removeAllListeners();
1681 mDesaturateAndDarkenAnimator.start();
1682 } else {
1683 mDesaturateAndDarkenAnimator.removeAllListeners();
1684 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1685 @Override
1686 public void onAnimationEnd(Animator animation) {
1687 super.onAnimationEnd(animation);
1688 // Stop using the animated paint.
1689 resetDesaturationAndDarken();
1690 }
1691 });
1692 mDesaturateAndDarkenAnimator.reverse();
1693 }
1694 }
1695
1696 private void resetDesaturationAndDarken() {
1697 mDesaturateAndDarkenAnimator.removeAllListeners();
1698 mDesaturateAndDarkenAnimator.cancel();
1699 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1700 }
1701
Lyn Han634483c2019-06-28 16:52:47 -07001702 /** Animates in the dismiss target. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001703 private void springInDismissTargetMaybe() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001704 if (mShowingDismiss) {
1705 return;
1706 }
1707
1708 mShowingDismiss = true;
1709
Joshua Tsuji20103542020-02-18 14:06:28 -05001710 mDismissTargetContainer.bringToFront();
1711 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
1712 mDismissTargetContainer.setVisibility(VISIBLE);
1713
1714 mDismissTargetAnimator.cancel();
1715 mDismissTargetAnimator
1716 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
1717 .start();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001718 }
1719
1720 /**
1721 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1722 * were dragged into the target and encircled.
1723 */
Lyn Han634483c2019-06-28 16:52:47 -07001724 private void hideDismissTarget() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001725 if (!mShowingDismiss) {
1726 return;
1727 }
1728
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001729 mShowingDismiss = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001730
Joshua Tsuji20103542020-02-18 14:06:28 -05001731 mDismissTargetAnimator
1732 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
1733 mDismissTargetSpring)
1734 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
1735 .start();
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001736 }
1737
Joshua Tsuji6549e702019-05-02 13:13:16 -04001738 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1739 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1740 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001741 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1742 // faster.
1743 mFlyoutTransitionSpring.getSpring().setStiffness(
1744 (mBubbleToExpandAfterFlyoutCollapse != null)
1745 ? SpringForce.STIFFNESS_MEDIUM
1746 : SpringForce.STIFFNESS_LOW);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001747 mFlyoutTransitionSpring
1748 .setStartValue(mFlyoutDragDeltaX)
1749 .setStartVelocity(velX)
1750 .animateToFinalPosition(collapsed
1751 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1752 : 0f);
1753 }
1754
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001755 /**
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001756 * Calculates the y position of the expanded view when it is expanded.
1757 */
Lyn Han285ad302019-05-29 19:01:39 -07001758 float getExpandedViewY() {
Lyn Han4a8efe32019-05-30 09:43:27 -07001759 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001760 }
1761
1762 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001763 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1764 */
1765 @VisibleForTesting
1766 void animateInFlyoutForBubble(Bubble bubble) {
Mady Mellordf898fd2020-01-09 09:26:36 -08001767 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001768 final BadgedImageView bubbleView = bubble.getIconView();
Mady Mellordf898fd2020-01-09 09:26:36 -08001769 if (flyoutMessage == null
1770 || flyoutMessage.message == null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001771 || !bubble.showFlyout()
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001772 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
Mark Renoufc19b4732019-06-26 12:08:33 -04001773 || isExpanded()
1774 || mIsExpansionAnimating
Joshua Tsuji14e68552019-06-06 17:17:08 -04001775 || mIsGestureInProgress
Lyn Hanf1f2c332019-08-23 17:06:56 -07001776 || mBubbleToExpandAfterFlyoutCollapse != null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001777 || bubbleView == null) {
1778 if (bubbleView != null) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001779 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001780 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001781 // Skip the message if none exists, we're expanded or animating expansion, or we're
Lyn Hanf1f2c332019-08-23 17:06:56 -07001782 // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
Mark Renoufc19b4732019-06-26 12:08:33 -04001783 return;
1784 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001785
Lyn Hanf1f2c332019-08-23 17:06:56 -07001786 mFlyoutDragDeltaX = 0f;
1787 clearFlyoutOnHide();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001788 mAfterFlyoutHidden = () -> {
1789 // Null it out to ensure it runs once.
1790 mAfterFlyoutHidden = null;
1791
1792 if (mBubbleToExpandAfterFlyoutCollapse != null) {
1793 // User tapped on the flyout and we should expand
1794 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1795 mBubbleData.setExpanded(true);
1796 mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001797 }
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001798
1799 // Stop suppressing the dot now that the flyout has morphed into the dot.
1800 bubbleView.removeDotSuppressionFlag(
1801 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Lyn Hanf1f2c332019-08-23 17:06:56 -07001802 };
1803 mFlyout.setVisibility(INVISIBLE);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001804
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001805 // Suppress the dot when we are animating the flyout.
1806 bubbleView.addDotSuppressionFlag(
1807 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001808
Lyn Hanf1f2c332019-08-23 17:06:56 -07001809 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1810 post(() -> {
1811 // An auto-expanding bubble could have been posted during the time it takes to
1812 // layout.
1813 if (isExpanded()) {
1814 return;
1815 }
1816 final Runnable expandFlyoutAfterDelay = () -> {
1817 mAnimateInFlyout = () -> {
1818 mFlyout.setVisibility(VISIBLE);
1819 mFlyoutDragDeltaX =
1820 mStackAnimationController.isStackOnLeftSide()
1821 ? -mFlyout.getWidth()
1822 : mFlyout.getWidth();
1823 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1824 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001825 };
Lyn Hanf1f2c332019-08-23 17:06:56 -07001826 mFlyout.postDelayed(mAnimateInFlyout, 200);
1827 };
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001828
1829 if (bubble.getIconView() == null) {
1830 return;
1831 }
1832
Mady Mellordf898fd2020-01-09 09:26:36 -08001833 mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
1834 mStackAnimationController.getStackPosition(), getWidth(),
Lyn Hanf1f2c332019-08-23 17:06:56 -07001835 mStackAnimationController.isStackOnLeftSide(),
Mady Mellor05e860b2019-10-30 22:48:15 -07001836 bubble.getIconView().getDotColor() /* dotColor */,
Lyn Hanf1f2c332019-08-23 17:06:56 -07001837 expandFlyoutAfterDelay /* onLayoutComplete */,
Mady Mellorb8aaf972019-11-26 10:28:00 -08001838 mAfterFlyoutHidden,
1839 bubble.getIconView().getDotCenter(),
1840 !bubble.showDot());
Lyn Hanf1f2c332019-08-23 17:06:56 -07001841 mFlyout.bringToFront();
1842 });
Mark Renoufc19b4732019-06-26 12:08:33 -04001843 mFlyout.removeCallbacks(mHideFlyout);
1844 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001845 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001846 }
1847
1848 /** Hide the flyout immediately and cancel any pending hide runnables. */
1849 private void hideFlyoutImmediate() {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001850 clearFlyoutOnHide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001851 mFlyout.removeCallbacks(mAnimateInFlyout);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001852 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001853 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001854 }
1855
Lyn Hanf1f2c332019-08-23 17:06:56 -07001856 private void clearFlyoutOnHide() {
1857 mFlyout.removeCallbacks(mAnimateInFlyout);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001858 if (mAfterFlyoutHidden == null) {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001859 return;
1860 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001861 mAfterFlyoutHidden.run();
1862 mAfterFlyoutHidden = null;
Lyn Hanf1f2c332019-08-23 17:06:56 -07001863 }
1864
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001865 @Override
1866 public void getBoundsOnScreen(Rect outRect) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001867 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1868 // When user education shows then capture all touches
1869 outRect.set(0, 0, getWidth(), getHeight());
1870 return;
1871 }
1872
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001873 if (!mIsExpanded) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001874 if (getBubbleCount() > 0) {
Mady Mellor217b2e92019-02-27 11:44:16 -08001875 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1876 }
Mady Mellore9371bc2019-07-10 18:50:59 -07001877 // Increase the touch target size of the bubble
1878 outRect.top -= mBubbleTouchPadding;
1879 outRect.left -= mBubbleTouchPadding;
1880 outRect.right += mBubbleTouchPadding;
1881 outRect.bottom += mBubbleTouchPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001882 } else {
1883 mBubbleContainer.getBoundsOnScreen(outRect);
1884 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001885
Joshua Tsuji6549e702019-05-02 13:13:16 -04001886 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001887 final Rect flyoutBounds = new Rect();
1888 mFlyout.getBoundsOnScreen(flyoutBounds);
1889 outRect.union(flyoutBounds);
1890 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001891 }
1892
1893 private int getStatusBarHeight() {
1894 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001895 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001896 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05001897 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001898 insets.getDisplayCutout() != null
1899 ? insets.getDisplayCutout().getSafeInsetTop()
1900 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001901 }
1902
1903 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001904 }
1905
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001906 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07001907 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001908 return;
1909 }
1910 mViewUpdatedRequested = true;
1911 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1912 invalidate();
1913 }
1914
1915 private void updateExpandedBubble() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001916 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001917 Log.d(TAG, "updateExpandedBubble()");
1918 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001919 mExpandedViewContainer.removeAllViews();
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001920 if (mIsExpanded && mExpandedBubble != null
1921 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -08001922 BubbleExpandedView bev = mExpandedBubble.getExpandedView();
Lyn Hana0bb02e2020-01-28 17:57:27 -08001923 mExpandedViewContainer.addView(bev);
1924 bev.populateExpandedView();
1925 mExpandedViewContainer.setVisibility(VISIBLE);
Issei Suzukic0387542019-03-08 17:31:14 +01001926 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001927 }
1928 }
1929
Lyn Han285ad302019-05-29 19:01:39 -07001930 private void updateExpandedView() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001931 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Han285ad302019-05-29 19:01:39 -07001932 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001933 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001934
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001935 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08001936 if (mIsExpanded) {
Lyn Han285ad302019-05-29 19:01:39 -07001937 final float y = getExpandedViewY();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001938 if (!mExpandedViewYAnim.isRunning()) {
1939 // We're not animating so set the value
1940 mExpandedViewContainer.setTranslationY(y);
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001941 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001942 mExpandedBubble.getExpandedView().updateView();
1943 }
Mady Mellor5d8f1402019-02-21 18:23:52 -08001944 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07001945 // We are animating so update the value; there is an end listener on the animator
1946 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08001947 mExpandedViewYAnim.animateToFinalPosition(y);
1948 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001949 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001950
Joshua Tsuji6549e702019-05-02 13:13:16 -04001951 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001952 updateBubbleZOrdersAndDotPosition(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001953 }
1954
1955 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001956 private void updateBubbleZOrdersAndDotPosition(boolean animate) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001957 int bubbleCount = getBubbleCount();
Lyn Han1b4f25e2019-06-11 13:56:34 -07001958 for (int i = 0; i < bubbleCount; i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001959 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellor70958542019-09-24 17:12:46 -07001960 bv.setZ((mMaxBubbles * mBubbleElevation) - i);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001961
Joshua Tsuji6549e702019-05-02 13:13:16 -04001962 // If the dot is on the left, and so is the stack, we need to change the dot position.
1963 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001964 bv.setDotPositionOnLeft(!mStackOnLeftOrWillBe, animate);
1965 }
1966
1967 if (!mIsExpanded && i > 0) {
1968 // If we're collapsed and this bubble is behind other bubbles, suppress its dot.
1969 bv.addDotSuppressionFlag(
1970 BadgedImageView.SuppressionFlag.BEHIND_STACK);
1971 } else {
1972 bv.removeDotSuppressionFlag(
1973 BadgedImageView.SuppressionFlag.BEHIND_STACK);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001974 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001975 }
1976 }
1977
Mady Mellorde2d4d22019-01-29 14:15:34 -08001978 private void updatePointerPosition() {
Mady Mellor2dce0fe2020-04-10 13:40:05 -07001979 if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
Lyn Han522e9ff2019-05-17 13:26:13 -07001980 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08001981 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001982 int index = getBubbleIndex(mExpandedBubble);
Lyn Hanf44562b2020-03-30 16:40:46 -07001983 if (index == -1) {
1984 return;
1985 }
Lyn Han522e9ff2019-05-17 13:26:13 -07001986 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1987 float halfBubble = mBubbleSize / 2f;
Mady Mellor9be3bed2019-08-21 17:26:26 -07001988 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
1989 // Padding might be adjusted for insets, so get it directly from the view
1990 bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
Lyn Han9f66c3b2020-03-05 23:59:29 -08001991 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08001992 }
1993
Steven Wua254dab2019-01-29 11:30:39 -05001994 /**
1995 * @return the number of bubbles in the stack view.
1996 */
Steven Wub00225b2019-02-08 14:27:42 -05001997 public int getBubbleCount() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001998 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1999 // Subtract 1 for the overflow button that is always in the bubble container.
2000 return mBubbleContainer.getChildCount() - 1;
2001 }
2002 return mBubbleContainer.getChildCount();
Steven Wua254dab2019-01-29 11:30:39 -05002003 }
2004
2005 /**
2006 * Finds the bubble index within the stack.
2007 *
Lyn Han3cd75d72020-02-15 19:10:12 -08002008 * @param provider the bubble view provider with the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05002009 * @return the index of the bubble view within the bubble stack. The range of the position
2010 * is between 0 and the bubble count minus 1.
2011 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002012 int getBubbleIndex(@Nullable BubbleViewProvider provider) {
Lyn Han9f66c3b2020-03-05 23:59:29 -08002013 if (provider == null) {
Steven Wua62cb6a2019-02-15 17:12:51 -05002014 return 0;
2015 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08002016 return mBubbleContainer.indexOfChild(provider.getIconView());
Steven Wua254dab2019-01-29 11:30:39 -05002017 }
2018
2019 /**
2020 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
2021 */
Steven Wub00225b2019-02-08 14:27:42 -05002022 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002023 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05002024 .setScale(4, RoundingMode.CEILING.HALF_UP)
2025 .floatValue();
2026 }
2027
2028 /**
2029 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
2030 */
Steven Wub00225b2019-02-08 14:27:42 -05002031 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002032 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05002033 .setScale(4, RoundingMode.CEILING.HALF_UP)
2034 .floatValue();
2035 }
2036
Joshua Tsujia19515f2019-02-13 18:02:29 -05002037 public PointF getStackPosition() {
2038 return mStackAnimationController.getStackPosition();
2039 }
2040
Steven Wua254dab2019-01-29 11:30:39 -05002041 /**
2042 * Logs the bubble UI event.
2043 *
2044 * @param bubble the bubble that is being interacted on. Null value indicates that
2045 * the user interaction is not specific to one bubble.
2046 * @param action the user interaction enum.
2047 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002048 private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
2049 if (bubble == null) {
2050 return;
Steven Wua254dab2019-01-29 11:30:39 -05002051 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002052 bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
2053 getNormalizedYPosition(), getBubbleIndex(bubble));
Steven Wua254dab2019-01-29 11:30:39 -05002054 }
Mark Renouf041d7262019-02-06 12:09:41 -05002055
2056 /**
2057 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
2058 * a back key down/up event pair is forwarded to the bubble Activity.
2059 */
2060 boolean performBackPressIfNeeded() {
Mady Mellor2dce0fe2020-04-10 13:40:05 -07002061 if (!isExpanded() || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
Mark Renouf041d7262019-02-06 12:09:41 -05002062 return false;
2063 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002064 return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
Mark Renouf041d7262019-02-06 12:09:41 -05002065 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002066
Mady Mellor5a3e94b2020-02-07 12:16:21 -08002067 /** Whether the educational view should appear for bubbles. **/
2068 private boolean shouldShowBubblesEducation() {
2069 return BubbleDebugConfig.forceShowUserEducation(getContext())
2070 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
2071 }
2072
2073 /** Whether the educational view should appear for the expanded view "manage" button. **/
2074 private boolean shouldShowManageEducation() {
2075 return BubbleDebugConfig.forceShowUserEducation(getContext())
2076 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
2077 }
2078
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002079 /** For debugging only */
2080 List<Bubble> getBubblesOnScreen() {
2081 List<Bubble> bubbles = new ArrayList<>();
Lyn Hanc47e1712020-01-28 21:43:34 -08002082 for (int i = 0; i < getBubbleCount(); i++) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002083 View child = mBubbleContainer.getChildAt(i);
Mady Mellorb8aaf972019-11-26 10:28:00 -08002084 if (child instanceof BadgedImageView) {
2085 String key = ((BadgedImageView) child).getKey();
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002086 Bubble bubble = mBubbleData.getBubbleWithKey(key);
2087 bubbles.add(bubble);
2088 }
2089 }
2090 return bubbles;
2091 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002092}