blob: 1329f04e55cdf27b11a94b5d403ac201cc577aef [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 Tsuji20103542020-02-18 14:06:28 -050086import com.android.systemui.util.DismissCircleView;
Joshua Tsuji7155bf12020-02-13 16:14:29 -050087import com.android.systemui.util.FloatingContentCoordinator;
Joshua Tsuji7dd88b02020-03-27 17:43:09 -040088import com.android.systemui.util.RelativeTouchListener;
Joshua Tsuji20103542020-02-18 14:06:28 -050089import com.android.systemui.util.animation.PhysicsAnimator;
90import com.android.systemui.util.magnetictarget.MagnetizedObject;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080091
Joshua Tsuji395bcfe2019-07-02 19:23:23 -040092import java.io.FileDescriptor;
93import java.io.PrintWriter;
Steven Wua254dab2019-01-29 11:30:39 -050094import java.math.BigDecimal;
95import java.math.RoundingMode;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040096import java.util.ArrayList;
Mark Renouf821e6782019-04-01 14:17:37 -040097import java.util.Collections;
98import java.util.List;
Steven Wua254dab2019-01-29 11:30:39 -050099
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800100/**
101 * Renders bubbles in a stack and handles animating expanded and collapsed states.
102 */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500103public class BubbleStackView extends FrameLayout {
Issei Suzukia8d07312019-06-07 12:56:19 +0200104 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleStackView" : TAG_BUBBLES;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800105
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800106 /** Animation durations for bubble stack user education views. **/
107 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION = 200;
108 private static final int ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT = 40;
109
Joshua Tsuji6549e702019-05-02 13:13:16 -0400110 /** How far the flyout needs to be dragged before it's dismissed regardless of velocity. */
111 static final float FLYOUT_DRAG_PERCENT_DISMISS = 0.25f;
112
113 /** Velocity required to dismiss the flyout via drag. */
114 private static final float FLYOUT_DISMISS_VELOCITY = 2000f;
115
116 /**
117 * Factor for attenuating translation when the flyout is overscrolled (8f = flyout moves 1 pixel
118 * for every 8 pixels overscrolled).
119 */
120 private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f;
121
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400122 /** Duration of the flyout alpha animations. */
123 private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
124
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400125 /** Percent to darken the bubbles when they're in the dismiss target. */
126 private static final float DARKEN_PERCENT = 0.3f;
127
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400128 /** How long to wait, in milliseconds, before hiding the flyout. */
129 @VisibleForTesting
130 static final int FLYOUT_HIDE_AFTER = 5000;
131
Joshua Tsujiff6b0f22020-03-09 14:55:19 -0400132 private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
133 new PhysicsAnimator.SpringConfig(
134 StackAnimationController.IME_ANIMATION_STIFFNESS,
135 StackAnimationController.DEFAULT_BOUNCINESS);
136
Issei Suzukic0387542019-03-08 17:31:14 +0100137 /**
138 * Interface to synchronize {@link View} state and the screen.
139 *
140 * {@hide}
141 */
142 interface SurfaceSynchronizer {
143 /**
144 * Wait until requested change on a {@link View} is reflected on the screen.
145 *
146 * @param callback callback to run after the change is reflected on the screen.
147 */
148 void syncSurfaceAndRun(Runnable callback);
149 }
150
151 private static final SurfaceSynchronizer DEFAULT_SURFACE_SYNCHRONIZER =
152 new SurfaceSynchronizer() {
153 @Override
154 public void syncSurfaceAndRun(Runnable callback) {
155 Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
156 // Just wait 2 frames. There is no guarantee, but this is usually enough time that
157 // the requested change is reflected on the screen.
158 // TODO: Once SurfaceFlinger provide APIs to sync the state of {@code View} and
159 // surfaces, rewrite this logic with them.
160 private int mFrameWait = 2;
161
162 @Override
163 public void doFrame(long frameTimeNanos) {
164 if (--mFrameWait > 0) {
165 Choreographer.getInstance().postFrameCallback(this);
166 } else {
167 callback.run();
168 }
169 }
170 });
171 }
172 };
173
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800174 private Point mDisplaySize;
175
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800176 private final SpringAnimation mExpandedViewXAnim;
177 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -0800178 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800179
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400180 private final Vibrator mVibrator;
181 private final ValueAnimator mDesaturateAndDarkenAnimator;
182 private final Paint mDesaturateAndDarkenPaint = new Paint();
183
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800184 private PhysicsAnimationLayout mBubbleContainer;
185 private StackAnimationController mStackAnimationController;
186 private ExpandedAnimationController mExpandedAnimationController;
187
Mady Mellor3dff9e62019-02-05 18:12:53 -0800188 private FrameLayout mExpandedViewContainer;
189
Joshua Tsuji6549e702019-05-02 13:13:16 -0400190 private BubbleFlyoutView mFlyout;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400191 /** Runnable that fades out the flyout and then sets it to GONE. */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400192 private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700193 /**
194 * Callback to run after the flyout hides. Also called if a new flyout is shown before the
195 * previous one animates out.
196 */
Mady Mellorb8aaf972019-11-26 10:28:00 -0800197 private Runnable mAfterFlyoutHidden;
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800198 /**
199 * Set when the flyout is tapped, so that we can expand the bubble associated with the flyout
200 * once it collapses.
201 */
202 @Nullable
203 private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400204
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400205 /** Layout change listener that moves the stack to the nearest valid position on rotation. */
Lyn Hanf4730312019-06-18 11:18:58 -0700206 private OnLayoutChangeListener mOrientationChangedListener;
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400207 /** Whether the stack was on the left side of the screen prior to rotation. */
208 private boolean mWasOnLeftBeforeRotation = false;
209 /**
210 * How far down the screen the stack was before rotation, in terms of percentage of the way down
211 * the allowable region. Defaults to -1 if not set.
212 */
213 private float mVerticalPosPercentBeforeRotation = -1;
214
Mady Mellor70958542019-09-24 17:12:46 -0700215 private int mMaxBubbles;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800216 private int mBubbleSize;
Mady Mellor70958542019-09-24 17:12:46 -0700217 private int mBubbleElevation;
Lyn Han4a8efe32019-05-30 09:43:27 -0700218 private int mBubblePaddingTop;
Mady Mellore9371bc2019-07-10 18:50:59 -0700219 private int mBubbleTouchPadding;
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700220 private int mExpandedViewPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800221 private int mExpandedAnimateXDistance;
222 private int mExpandedAnimateYDistance;
Lyn Han5aa27e22019-05-15 10:55:07 -0700223 private int mPointerHeight;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500224 private int mStatusBarHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500225 private int mImeOffset;
Lyn Han3cd75d72020-02-15 19:10:12 -0800226 private BubbleViewProvider mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800227 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800228
Joshua Tsuji6549e702019-05-02 13:13:16 -0400229 /** Whether the stack is currently on the left side of the screen, or animating there. */
Joshua Tsuji2ed260e2020-03-26 14:26:01 -0400230 private boolean mStackOnLeftOrWillBe = true;
Joshua Tsuji6549e702019-05-02 13:13:16 -0400231
232 /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
233 private boolean mIsGestureInProgress = false;
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400234
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400235 /** Description of current animation controller state. */
236 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
237 pw.println("Stack view state:");
238 pw.print(" gestureInProgress: "); pw.println(mIsGestureInProgress);
239 pw.print(" showingDismiss: "); pw.println(mShowingDismiss);
240 pw.print(" isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
Joshua Tsuji395bcfe2019-07-02 19:23:23 -0400241 mStackAnimationController.dump(fd, pw, args);
242 mExpandedAnimationController.dump(fd, pw, args);
243 }
244
Mady Mellorcd9b1302018-11-06 18:08:04 -0800245 private BubbleController.BubbleExpandListener mExpandListener;
Joshua Tsujibe60a582020-03-23 17:17:26 -0400246 private SysUiState mSysUiState;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800247
248 private boolean mViewUpdatedRequested = false;
Mady Mellorbc078c22019-03-26 17:10:34 -0700249 private boolean mIsExpansionAnimating = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400250 private boolean mShowingDismiss = false;
251
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400252 /** The view to desaturate/darken when magneted to the dismiss target. */
253 private View mDesaturateAndDarkenTargetView;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800254
Mady Mellor3dff9e62019-02-05 18:12:53 -0800255 private LayoutInflater mInflater;
256
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800257 // Used for determining view / touch intersection
258 int[] mTempLoc = new int[2];
259 RectF mTempRect = new RectF();
260
Mark Renouf821e6782019-04-01 14:17:37 -0400261 private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
262
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800263 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
264 new ViewTreeObserver.OnPreDrawListener() {
265 @Override
266 public boolean onPreDraw() {
267 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
Lyn Han285ad302019-05-29 19:01:39 -0700268 updateExpandedView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800269 mViewUpdatedRequested = false;
270 return true;
271 }
272 };
273
Mark Renouf821e6782019-04-01 14:17:37 -0400274 private ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
275 this::updateSystemGestureExcludeRects;
276
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800277 private ViewClippingUtil.ClippingParameters mClippingParameters =
278 new ViewClippingUtil.ClippingParameters() {
279
Lyn Han522e9ff2019-05-17 13:26:13 -0700280 @Override
281 public boolean shouldFinish(View view) {
282 return false;
283 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800284
Lyn Han522e9ff2019-05-17 13:26:13 -0700285 @Override
286 public boolean isClippingEnablingAllowed(View view) {
287 return !mIsExpanded;
288 }
289 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800290
Joshua Tsuji6549e702019-05-02 13:13:16 -0400291 /** Float property that 'drags' the flyout. */
292 private final FloatPropertyCompat mFlyoutCollapseProperty =
293 new FloatPropertyCompat("FlyoutCollapseSpring") {
294 @Override
295 public float getValue(Object o) {
296 return mFlyoutDragDeltaX;
297 }
298
299 @Override
300 public void setValue(Object o, float v) {
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400301 setFlyoutStateForDragLength(v);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400302 }
303 };
304
305 /** SpringAnimation that springs the flyout collapsed via onFlyoutDragged. */
306 private final SpringAnimation mFlyoutTransitionSpring =
307 new SpringAnimation(this, mFlyoutCollapseProperty);
308
309 /** Distance the flyout has been dragged in the X axis. */
310 private float mFlyoutDragDeltaX = 0f;
311
312 /**
Joshua Tsuji14e68552019-06-06 17:17:08 -0400313 * Runnable that animates in the flyout. This reference is needed to cancel delayed postings.
314 */
315 private Runnable mAnimateInFlyout;
316
317 /**
Joshua Tsuji6549e702019-05-02 13:13:16 -0400318 * End listener for the flyout spring that either posts a runnable to hide the flyout, or hides
319 * it immediately.
320 */
321 private final DynamicAnimation.OnAnimationEndListener mAfterFlyoutTransitionSpring =
322 (dynamicAnimation, b, v, v1) -> {
323 if (mFlyoutDragDeltaX == 0) {
324 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
325 } else {
326 mFlyout.hideFlyout();
327 }
328 };
329
Lyn Han1b4f25e2019-06-11 13:56:34 -0700330 @NonNull
331 private final SurfaceSynchronizer mSurfaceSynchronizer;
Issei Suzukic0387542019-03-08 17:31:14 +0100332
Joshua Tsuji20103542020-02-18 14:06:28 -0500333 /**
334 * The currently magnetized object, which is being dragged and will be attracted to the magnetic
335 * dismiss target.
336 *
337 * This is either the stack itself, or an individual bubble.
338 */
339 private MagnetizedObject<?> mMagnetizedObject;
340
341 /**
Joshua Tsuji20103542020-02-18 14:06:28 -0500342 * The MagneticTarget instance for our circular dismiss view. This is added to the
343 * MagnetizedObject instances for the stack and any dragged-out bubbles.
344 */
345 private MagnetizedObject.MagneticTarget mMagneticTarget;
346
347 /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
348 private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
349 new MagnetizedObject.MagnetListener() {
350 @Override
351 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
352 animateDesaturateAndDarken(
353 mExpandedAnimationController.getDraggedOutBubble(), true);
354 }
355
356 @Override
357 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
358 float velX, float velY, boolean wasFlungOut) {
359 animateDesaturateAndDarken(
360 mExpandedAnimationController.getDraggedOutBubble(), false);
361
362 if (wasFlungOut) {
363 mExpandedAnimationController.snapBubbleBack(
364 mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
365 hideDismissTarget();
366 } else {
367 mExpandedAnimationController.onUnstuckFromTarget();
368 }
369 }
370
371 @Override
372 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
373 mExpandedAnimationController.dismissDraggedOutBubble(
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400374 mExpandedAnimationController.getDraggedOutBubble() /* bubble */,
375 mDismissTargetContainer.getHeight() /* translationYBy */,
376 BubbleStackView.this::dismissMagnetizedObject /* after */);
Joshua Tsuji20103542020-02-18 14:06:28 -0500377 hideDismissTarget();
378 }
379 };
380
381 /** Magnet listener that handles animating and dismissing the entire stack. */
382 private final MagnetizedObject.MagnetListener mStackMagnetListener =
383 new MagnetizedObject.MagnetListener() {
384 @Override
385 public void onStuckToTarget(
386 @NonNull MagnetizedObject.MagneticTarget target) {
387 animateDesaturateAndDarken(mBubbleContainer, true);
388 }
389
390 @Override
391 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
392 float velX, float velY, boolean wasFlungOut) {
393 animateDesaturateAndDarken(mBubbleContainer, false);
394
395 if (wasFlungOut) {
396 mStackAnimationController.flingStackThenSpringToEdge(
397 mStackAnimationController.getStackPosition().x, velX, velY);
398 hideDismissTarget();
399 } else {
400 mStackAnimationController.onUnstuckFromTarget();
401 }
402 }
403
404 @Override
405 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
Joshua Tsuji79a58ee2020-03-27 17:55:37 -0400406 mStackAnimationController.animateStackDismissal(
407 mDismissTargetContainer.getHeight() /* translationYBy */,
Joshua Tsuji20103542020-02-18 14:06:28 -0500408 () -> {
409 resetDesaturationAndDarken();
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400410 dismissMagnetizedObject();
Joshua Tsuji20103542020-02-18 14:06:28 -0500411 }
412 );
413
414 hideDismissTarget();
415 }
416 };
417
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400418 /**
419 * Click listener set on each bubble view. When collapsed, clicking a bubble expands the stack.
420 * When expanded, clicking a bubble either expands that bubble, or collapses the stack.
421 */
422 private OnClickListener mBubbleClickListener = new OnClickListener() {
423 @Override
424 public void onClick(View view) {
425 final Bubble clickedBubble = mBubbleData.getBubbleWithView(view);
426
427 // If the bubble has since left us, ignore the click.
428 if (clickedBubble == null) {
429 return;
430 }
431
432 final boolean clickedBubbleIsCurrentlyExpandedBubble =
433 clickedBubble.getKey().equals(mExpandedBubble.getKey());
434
435 if (isExpanded() && !clickedBubbleIsCurrentlyExpandedBubble) {
436 if (clickedBubble != mBubbleData.getSelectedBubble()) {
437 // Select the clicked bubble.
438 mBubbleData.setSelectedBubble(clickedBubble);
439 } else {
440 // If the clicked bubble is the selected bubble (but not the expanded bubble),
441 // that means overflow was previously expanded. Set the selected bubble
442 // internally without going through BubbleData (which would ignore it since it's
443 // already selected).
444 setSelectedBubble(clickedBubble);
445
446 }
447 } else {
448 // Otherwise, we either tapped the stack (which means we're collapsed
449 // and should expand) or the currently selected bubble (we're expanded
450 // and should collapse).
451 if (!maybeShowStackUserEducation()) {
452 mBubbleData.setExpanded(!mBubbleData.isExpanded());
453 }
454 }
455 }
456 };
457
458 /**
459 * Touch listener set on each bubble view. This enables dragging and dismissing the stack (when
460 * collapsed), or individual bubbles (when expanded).
461 */
462 private RelativeTouchListener mBubbleTouchListener = new RelativeTouchListener() {
463
464 @Override
465 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
466 // If we're expanding or collapsing, consume but ignore all touch events.
467 if (mIsExpansionAnimating) {
468 return true;
469 }
470
471 if (mBubbleData.isExpanded()) {
472 maybeShowManageEducation(false /* show */);
473
474 // If we're expanded, tell the animation controller to prepare to drag this bubble,
475 // dispatching to the individual bubble magnet listener.
476 mExpandedAnimationController.prepareForBubbleDrag(
477 v /* bubble */,
478 mMagneticTarget,
479 mIndividualBubbleMagnetListener);
480
481 // Save the magnetized individual bubble so we can dispatch touch events to it.
482 mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
483 } else {
484 // If we're collapsed, prepare to drag the stack. Cancel active animations, set the
485 // animation controller, and hide the flyout.
486 mStackAnimationController.cancelStackPositionAnimations();
487 mBubbleContainer.setActiveController(mStackAnimationController);
488 hideFlyoutImmediate();
489
490 // Also, save the magnetized stack so we can dispatch touch events to it.
491 mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
492 mMagnetizedObject.setMagnetListener(mStackMagnetListener);
493 }
494
495 passEventToMagnetizedObject(ev);
496
497 // Bubbles are always interested in all touch events!
498 return true;
499 }
500
501 @Override
502 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
503 float viewInitialY, float dx, float dy) {
504 // If we're expanding or collapsing, ignore all touch events.
505 if (mIsExpansionAnimating) {
506 return;
507 }
508
509 // Show the dismiss target, if we haven't already.
510 springInDismissTargetMaybe();
511
512 // First, see if the magnetized object consumes the event - if so, we shouldn't move the
513 // bubble since it's stuck to the target.
514 if (!passEventToMagnetizedObject(ev)) {
515 if (mBubbleData.isExpanded()) {
516 mExpandedAnimationController.dragBubbleOut(
517 v, viewInitialX + dx, viewInitialY + dy);
518 } else {
519 hideStackUserEducation(false /* fromExpansion */);
520 mStackAnimationController.moveStackFromTouch(
521 viewInitialX + dx, viewInitialY + dy);
522 }
523 }
524 }
525
526 @Override
527 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
528 float viewInitialY, float dx, float dy, float velX, float velY) {
529 // If we're expanding or collapsing, ignore all touch events.
530 if (mIsExpansionAnimating) {
531 return;
532 }
533
534 // First, see if the magnetized object consumes the event - if so, the bubble was
535 // released in the target or flung out of it, and we should ignore the event.
536 if (!passEventToMagnetizedObject(ev)) {
537 if (mBubbleData.isExpanded()) {
538 mExpandedAnimationController.snapBubbleBack(v, velX, velY);
539 } else {
540 // Fling the stack to the edge, and save whether or not it's going to end up on
541 // the left side of the screen.
542 mStackOnLeftOrWillBe =
543 mStackAnimationController.flingStackThenSpringToEdge(
544 viewInitialX + dx, velX, velY) <= 0;
545
546 updateBubbleZOrdersAndDotPosition(true /* animate */);
547
548 logBubbleEvent(null /* no bubble associated with bubble stack move */,
549 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
550 }
551
552 hideDismissTarget();
553 }
554 }
555 };
556
557 /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
558 private OnClickListener mFlyoutClickListener = new OnClickListener() {
559 @Override
560 public void onClick(View view) {
561 if (maybeShowStackUserEducation()) {
562 // If we're showing user education, don't open the bubble show the education first
563 mBubbleToExpandAfterFlyoutCollapse = null;
564 } else {
565 mBubbleToExpandAfterFlyoutCollapse = mBubbleData.getSelectedBubble();
566 }
567
568 mFlyout.removeCallbacks(mHideFlyout);
569 mHideFlyout.run();
570 }
571 };
572
573 /** Touch listener for the flyout. This enables the drag-to-dismiss gesture on the flyout. */
574 private RelativeTouchListener mFlyoutTouchListener = new RelativeTouchListener() {
575
576 @Override
577 public boolean onDown(@NonNull View v, @NonNull MotionEvent ev) {
578 mFlyout.removeCallbacks(mHideFlyout);
579 return true;
580 }
581
582 @Override
583 public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
584 float viewInitialY, float dx, float dy) {
585 setFlyoutStateForDragLength(dx);
586 }
587
588 @Override
589 public void onUp(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
590 float viewInitialY, float dx, float dy, float velX, float velY) {
591 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
592 final boolean metRequiredVelocity =
593 onLeft ? velX < -FLYOUT_DISMISS_VELOCITY : velX > FLYOUT_DISMISS_VELOCITY;
594 final boolean metRequiredDeltaX =
595 onLeft
596 ? dx < -mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS
597 : dx > mFlyout.getWidth() * FLYOUT_DRAG_PERCENT_DISMISS;
598 final boolean isCancelFling = onLeft ? velX > 0 : velX < 0;
599 final boolean shouldDismiss = metRequiredVelocity
600 || (metRequiredDeltaX && !isCancelFling);
601
602 mFlyout.removeCallbacks(mHideFlyout);
603 animateFlyoutCollapsed(shouldDismiss, velX);
604
605 maybeShowStackUserEducation();
606 }
607 };
608
Joshua Tsuji20103542020-02-18 14:06:28 -0500609 private ViewGroup mDismissTargetContainer;
610 private PhysicsAnimator<View> mDismissTargetAnimator;
611 private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
612 SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
Issei Suzukic0387542019-03-08 17:31:14 +0100613
Lyn Hanf4730312019-06-18 11:18:58 -0700614 private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
Lyn Han3cd75d72020-02-15 19:10:12 -0800615
Joshua Tsujia2433db2020-03-12 17:56:22 -0400616 @Nullable
Lyn Han3cd75d72020-02-15 19:10:12 -0800617 private BubbleOverflow mBubbleOverflow;
Lyn Hanf4730312019-06-18 11:18:58 -0700618
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800619 private boolean mShouldShowUserEducation;
620 private boolean mAnimatingEducationAway;
621 private View mUserEducationView;
622
623 private boolean mShouldShowManageEducation;
624 private BubbleManageEducationView mManageEducationView;
625 private boolean mAnimatingManageEducationAway;
626
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400627 @SuppressLint("ClickableViewAccessibility")
Issei Suzukic0387542019-03-08 17:31:14 +0100628 public BubbleStackView(Context context, BubbleData data,
Joshua Tsuji7155bf12020-02-13 16:14:29 -0500629 @Nullable SurfaceSynchronizer synchronizer,
Joshua Tsujibe60a582020-03-23 17:17:26 -0400630 FloatingContentCoordinator floatingContentCoordinator,
631 SysUiState sysUiState) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800632 super(context);
633
Mady Mellorcfd06c12019-02-13 14:32:12 -0800634 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800635 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800636
Joshua Tsujibe60a582020-03-23 17:17:26 -0400637 mSysUiState = sysUiState;
638
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800639 Resources res = getResources();
Mady Mellor70958542019-09-24 17:12:46 -0700640 mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800641 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellor70958542019-09-24 17:12:46 -0700642 mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Lyn Han4a8efe32019-05-30 09:43:27 -0700643 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
Mady Mellore9371bc2019-07-10 18:50:59 -0700644 mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800645 mExpandedAnimateXDistance =
646 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
647 mExpandedAnimateYDistance =
648 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Lyn Han5aa27e22019-05-15 10:55:07 -0700649 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
650
Joshua Tsujif44347f2019-02-12 14:28:06 -0500651 mStatusBarHeight =
652 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500653 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800654
655 mDisplaySize = new Point();
656 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Mady Mellor9be3bed2019-08-21 17:26:26 -0700657 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700658 wm.getDefaultDisplay().getRealSize(mDisplaySize);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800659
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400660 mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
661
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700662 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800663 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800664
Joshua Tsuji259c66b82020-03-16 14:40:41 -0400665 mStackAnimationController = new StackAnimationController(
666 floatingContentCoordinator, this::getBubbleCount);
Lyn Hanf4730312019-06-18 11:18:58 -0700667
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700668 mExpandedAnimationController = new ExpandedAnimationController(
Lyn Hanf4730312019-06-18 11:18:58 -0700669 mDisplaySize, mExpandedViewPadding, res.getConfiguration().orientation);
Issei Suzukic0387542019-03-08 17:31:14 +0100670 mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER;
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800671
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800672 setUpUserEducation();
673
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800674 mBubbleContainer = new PhysicsAnimationLayout(context);
Joshua Tsujic36ee6f2019-05-28 17:00:16 -0400675 mBubbleContainer.setActiveController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800676 mBubbleContainer.setElevation(elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800677 mBubbleContainer.setClipChildren(false);
678 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
679
Mady Mellor3dff9e62019-02-05 18:12:53 -0800680 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800681 mExpandedViewContainer.setElevation(elevation);
Lyn Han6f6b3ae2019-05-16 14:17:30 -0700682 mExpandedViewContainer.setPadding(mExpandedViewPadding, mExpandedViewPadding,
683 mExpandedViewPadding, mExpandedViewPadding);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800684 mExpandedViewContainer.setClipChildren(false);
685 addView(mExpandedViewContainer);
686
Mady Mellor8bfe5412019-07-31 14:56:44 -0700687 setUpFlyout();
Joshua Tsuji6549e702019-05-02 13:13:16 -0400688 mFlyoutTransitionSpring.setSpring(new SpringForce()
Joshua Tsuji14e68552019-06-06 17:17:08 -0400689 .setStiffness(SpringForce.STIFFNESS_LOW)
Joshua Tsuji6549e702019-05-02 13:13:16 -0400690 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
691 mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
692
Joshua Tsujie48c4112020-02-26 14:36:25 -0500693 final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
Joshua Tsuji20103542020-02-18 14:06:28 -0500694 final View targetView = new DismissCircleView(context);
695 final FrameLayout.LayoutParams newParams =
Joshua Tsujie48c4112020-02-26 14:36:25 -0500696 new FrameLayout.LayoutParams(targetSize, targetSize);
Joshua Tsuji20103542020-02-18 14:06:28 -0500697 newParams.gravity = Gravity.CENTER;
698 targetView.setLayoutParams(newParams);
699 mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
700
701 mDismissTargetContainer = new FrameLayout(context);
702 mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
Joshua Tsuji6549e702019-05-02 13:13:16 -0400703 MATCH_PARENT,
Joshua Tsujif39539d2020-04-03 18:53:06 -0400704 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
Joshua Tsuji6549e702019-05-02 13:13:16 -0400705 Gravity.BOTTOM));
Joshua Tsuji20103542020-02-18 14:06:28 -0500706 mDismissTargetContainer.setClipChildren(false);
707 mDismissTargetContainer.addView(targetView);
708 mDismissTargetContainer.setVisibility(View.INVISIBLE);
709 addView(mDismissTargetContainer);
710
711 // Start translated down so the target springs up.
712 targetView.setTranslationY(
Joshua Tsujif39539d2020-04-03 18:53:06 -0400713 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height));
Joshua Tsuji20103542020-02-18 14:06:28 -0500714
715 // Save the MagneticTarget instance for the newly set up view - we'll add this to the
716 // MagnetizedObjects.
717 mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400718
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800719 mExpandedViewXAnim =
720 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
721 mExpandedViewXAnim.setSpring(
722 new SpringForce()
723 .setStiffness(SpringForce.STIFFNESS_LOW)
724 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
725
726 mExpandedViewYAnim =
727 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
728 mExpandedViewYAnim.setSpring(
729 new SpringForce()
730 .setStiffness(SpringForce.STIFFNESS_LOW)
731 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorbc078c22019-03-26 17:10:34 -0700732 mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700733 if (mIsExpanded && mExpandedBubble != null
734 && mExpandedBubble.getExpandedView() != null) {
Lyn Han3cd75d72020-02-15 19:10:12 -0800735 mExpandedBubble.getExpandedView().updateView();
Mady Mellorbc078c22019-03-26 17:10:34 -0700736 }
737 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800738
739 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800740 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500741 mBubbleContainer.bringToFront();
Mady Mellor5d8f1402019-02-21 18:23:52 -0800742
Lyn Hancd4f87e2020-02-19 20:33:45 -0800743 setUpOverflow();
Lyn Hanb58c7562020-01-07 14:29:20 -0800744
Mady Mellor5d8f1402019-02-21 18:23:52 -0800745 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
Mady Mellorbc078c22019-03-26 17:10:34 -0700746 if (!mIsExpanded || mIsExpansionAnimating) {
Mady Mellor5d8f1402019-02-21 18:23:52 -0800747 return view.onApplyWindowInsets(insets);
748 }
Mady Mellor5d8f1402019-02-21 18:23:52 -0800749 mExpandedAnimationController.updateYPosition(
750 // Update the insets after we're done translating otherwise position
751 // calculation for them won't be correct.
Lyn Hanb58c7562020-01-07 14:29:20 -0800752 () -> {
Mady Melloradd5c6a92020-03-31 17:22:48 -0700753 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800754 mExpandedBubble.getExpandedView().updateInsets(insets);
755 }
756 });
Mady Mellor5d8f1402019-02-21 18:23:52 -0800757 return view.onApplyWindowInsets(insets);
758 });
Mark Renouf821e6782019-04-01 14:17:37 -0400759
Lyn Hanf4730312019-06-18 11:18:58 -0700760 mOrientationChangedListener =
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400761 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
Mady Mellor9be3bed2019-08-21 17:26:26 -0700762 mExpandedAnimationController.updateOrientation(mOrientation, mDisplaySize);
763 mStackAnimationController.updateOrientation(mOrientation);
764
765 // Reposition & adjust the height for new orientation
766 if (mIsExpanded) {
767 mExpandedViewContainer.setTranslationY(getExpandedViewY());
Mady Melloradd5c6a92020-03-31 17:22:48 -0700768 if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -0800769 mExpandedBubble.getExpandedView().updateView();
770 }
Mady Mellor9be3bed2019-08-21 17:26:26 -0700771 }
772
773 // Need to update the padding around the view
774 WindowInsets insets = getRootWindowInsets();
775 int leftPadding = mExpandedViewPadding;
776 int rightPadding = mExpandedViewPadding;
777 if (insets != null) {
778 // Can't have the expanded view overlaying notches
779 int cutoutLeft = 0;
780 int cutoutRight = 0;
781 DisplayCutout cutout = insets.getDisplayCutout();
782 if (cutout != null) {
783 cutoutLeft = cutout.getSafeInsetLeft();
784 cutoutRight = cutout.getSafeInsetRight();
785 }
786 // Or overlaying nav or status bar
787 leftPadding += Math.max(cutoutLeft, insets.getStableInsetLeft());
788 rightPadding += Math.max(cutoutRight, insets.getStableInsetRight());
789 }
790 mExpandedViewContainer.setPadding(leftPadding, mExpandedViewPadding,
791 rightPadding, mExpandedViewPadding);
792
Lyn Hanf4730312019-06-18 11:18:58 -0700793 if (mIsExpanded) {
794 // Re-draw bubble row and pointer for new orientation.
795 mExpandedAnimationController.expandFromStack(() -> {
796 updatePointerPosition();
797 } /* after */);
798 }
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400799 if (mVerticalPosPercentBeforeRotation >= 0) {
800 mStackAnimationController.moveStackToSimilarPositionAfterRotation(
801 mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation);
802 }
Lyn Hanf4730312019-06-18 11:18:58 -0700803 removeOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400804 };
805
Mark Renouf821e6782019-04-01 14:17:37 -0400806 // This must be a separate OnDrawListener since it should be called for every draw.
807 getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater);
Joshua Tsuji4accf5982019-04-22 17:36:11 -0400808
809 final ColorMatrix animatedMatrix = new ColorMatrix();
810 final ColorMatrix darkenMatrix = new ColorMatrix();
811
812 mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f);
813 mDesaturateAndDarkenAnimator.addUpdateListener(animation -> {
814 final float animatedValue = (float) animation.getAnimatedValue();
815 animatedMatrix.setSaturation(animatedValue);
816
817 final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT;
818 darkenMatrix.setScale(
819 1f - animatedDarkenValue /* red */,
820 1f - animatedDarkenValue /* green */,
821 1f - animatedDarkenValue /* blue */,
822 1f /* alpha */);
823
824 // Concat the matrices so that the animatedMatrix both desaturates and darkens.
825 animatedMatrix.postConcat(darkenMatrix);
826
827 // Update the paint and apply it to the bubble container.
828 mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
829 mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
830 });
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400831
832 // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts,
833 // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
834 setOnTouchListener((view, ev) -> {
835 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
836 if (mBubbleData.isExpanded()) {
837 mBubbleData.setExpanded(false);
838 }
839 }
840
841 return false;
842 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800843 }
844
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800845 private void setUpUserEducation() {
846 if (mUserEducationView != null) {
847 removeView(mUserEducationView);
848 }
849 mShouldShowUserEducation = shouldShowBubblesEducation();
850 if (DEBUG_USER_EDUCATION) {
851 Log.d(TAG, "shouldShowUserEducation: " + mShouldShowUserEducation);
852 }
853 if (mShouldShowUserEducation) {
854 mUserEducationView = mInflater.inflate(R.layout.bubble_stack_user_education, this,
855 false /* attachToRoot */);
856 mUserEducationView.setVisibility(GONE);
857
858 final TypedArray ta = mContext.obtainStyledAttributes(
859 new int[] {android.R.attr.colorAccent,
860 android.R.attr.textColorPrimaryInverse});
861 final int bgColor = ta.getColor(0, Color.BLACK);
862 int textColor = ta.getColor(1, Color.WHITE);
863 ta.recycle();
864 textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true);
865
866 TextView title = mUserEducationView.findViewById(R.id.user_education_title);
867 TextView description = mUserEducationView.findViewById(R.id.user_education_description);
868 title.setTextColor(textColor);
869 description.setTextColor(textColor);
870
871 addView(mUserEducationView);
872 }
873
874 if (mManageEducationView != null) {
875 removeView(mManageEducationView);
876 }
877 mShouldShowManageEducation = shouldShowManageEducation();
878 if (DEBUG_USER_EDUCATION) {
879 Log.d(TAG, "shouldShowManageEducation: " + mShouldShowManageEducation);
880 }
881 if (mShouldShowManageEducation) {
882 mManageEducationView = (BubbleManageEducationView)
883 mInflater.inflate(R.layout.bubbles_manage_button_education, this,
884 false /* attachToRoot */);
885 mManageEducationView.setVisibility(GONE);
886 mManageEducationView.setElevation(mBubbleElevation);
887
888 addView(mManageEducationView);
Lyn Hanb58c7562020-01-07 14:29:20 -0800889 }
Lyn Hanb58c7562020-01-07 14:29:20 -0800890 }
891
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400892 @SuppressLint("ClickableViewAccessibility")
Mady Mellor8bfe5412019-07-31 14:56:44 -0700893 private void setUpFlyout() {
894 if (mFlyout != null) {
895 removeView(mFlyout);
896 }
897 mFlyout = new BubbleFlyoutView(getContext());
898 mFlyout.setVisibility(GONE);
899 mFlyout.animate()
900 .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
901 .setInterpolator(new AccelerateDecelerateInterpolator());
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400902 mFlyout.setOnClickListener(mFlyoutClickListener);
903 mFlyout.setOnTouchListener(mFlyoutTouchListener);
Mady Mellor8bfe5412019-07-31 14:56:44 -0700904 addView(mFlyout, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
905 }
906
Lyn Hancd4f87e2020-02-19 20:33:45 -0800907 private void setUpOverflow() {
Lyn Han8cc4bf82020-03-05 16:34:37 -0800908 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
909 return;
910 }
Lyn Hancd4f87e2020-02-19 20:33:45 -0800911 int overflowBtnIndex = 0;
912 if (mBubbleOverflow == null) {
Mady Mellor0122cc92020-02-27 12:15:39 -0800913 mBubbleOverflow = new BubbleOverflow(getContext());
914 mBubbleOverflow.setUpOverflow(mBubbleContainer, this);
Lyn Hancd4f87e2020-02-19 20:33:45 -0800915 } else {
916 mBubbleContainer.removeView(mBubbleOverflow.getBtn());
917 mBubbleOverflow.updateIcon(mContext, this);
Lyn Hane1cf3b22020-04-01 13:39:43 -0700918 overflowBtnIndex = mBubbleContainer.getChildCount();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800919 }
920 mBubbleContainer.addView(mBubbleOverflow.getBtn(), overflowBtnIndex,
921 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
922
Joshua Tsuji7dd88b02020-03-27 17:43:09 -0400923 mBubbleOverflow.getBtn().setOnClickListener(v -> setSelectedBubble(mBubbleOverflow));
Lyn Hancd4f87e2020-02-19 20:33:45 -0800924 }
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700925 /**
Lyn Han02cca812019-04-02 16:27:32 -0700926 * Handle theme changes.
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700927 */
Lyn Han02cca812019-04-02 16:27:32 -0700928 public void onThemeChanged() {
Mady Mellor8bfe5412019-07-31 14:56:44 -0700929 setUpFlyout();
Lyn Hancd4f87e2020-02-19 20:33:45 -0800930 setUpOverflow();
Mady Mellor5a3e94b2020-02-07 12:16:21 -0800931 setUpUserEducation();
Lyn Hanf1c9b8b2019-03-14 16:49:48 -0700932 }
933
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400934 /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
Lyn Hanf4730312019-06-18 11:18:58 -0700935 public void onOrientationChanged(int orientation) {
936 mOrientation = orientation;
937
Mady Mellore19353d2019-08-21 17:25:02 -0700938 // Display size is based on the rotation device was in when requested, we should update it
Mady Mellor9be3bed2019-08-21 17:26:26 -0700939 // We use the real size & subtract screen decorations / window insets ourselves when needed
Mady Mellore19353d2019-08-21 17:25:02 -0700940 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
941 wm.getDefaultDisplay().getRealSize(mDisplaySize);
942
Mady Mellor818eef02019-08-16 16:12:29 -0700943 // Some resources change depending on orientation
944 Resources res = getContext().getResources();
945 mStatusBarHeight = res.getDimensionPixelSize(
946 com.android.internal.R.dimen.status_bar_height);
947 mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
948
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400949 final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion();
950 mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide();
951 mVerticalPosPercentBeforeRotation =
952 (mStackAnimationController.getStackPosition().y - allowablePos.top)
953 / (allowablePos.bottom - allowablePos.top);
Lyn Hanf4730312019-06-18 11:18:58 -0700954 addOnLayoutChangeListener(mOrientationChangedListener);
Joshua Tsujif418f9e2019-04-04 17:09:53 -0400955 hideFlyoutImmediate();
956 }
957
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800958 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800959 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
960 getBoundsOnScreen(outRect);
961 }
962
963 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800964 protected void onDetachedFromWindow() {
965 super.onDetachedFromWindow();
966 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
967 }
968
969 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800970 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
971 super.onInitializeAccessibilityNodeInfoInternal(info);
Lyn Hane68d0912019-05-02 18:28:01 -0700972
973 // Custom actions.
974 AccessibilityAction moveTopLeft = new AccessibilityAction(R.id.action_move_top_left,
975 getContext().getResources()
976 .getString(R.string.bubble_accessibility_action_move_top_left));
977 info.addAction(moveTopLeft);
978
979 AccessibilityAction moveTopRight = new AccessibilityAction(R.id.action_move_top_right,
980 getContext().getResources()
981 .getString(R.string.bubble_accessibility_action_move_top_right));
982 info.addAction(moveTopRight);
983
984 AccessibilityAction moveBottomLeft = new AccessibilityAction(R.id.action_move_bottom_left,
985 getContext().getResources()
986 .getString(R.string.bubble_accessibility_action_move_bottom_left));
987 info.addAction(moveBottomLeft);
988
989 AccessibilityAction moveBottomRight = new AccessibilityAction(R.id.action_move_bottom_right,
990 getContext().getResources()
991 .getString(R.string.bubble_accessibility_action_move_bottom_right));
992 info.addAction(moveBottomRight);
993
994 // Default actions.
995 info.addAction(AccessibilityAction.ACTION_DISMISS);
Mady Mellor217b2e92019-02-27 11:44:16 -0800996 if (mIsExpanded) {
Lyn Hane68d0912019-05-02 18:28:01 -0700997 info.addAction(AccessibilityAction.ACTION_COLLAPSE);
Mady Mellor217b2e92019-02-27 11:44:16 -0800998 } else {
Lyn Hane68d0912019-05-02 18:28:01 -0700999 info.addAction(AccessibilityAction.ACTION_EXPAND);
Mady Mellor217b2e92019-02-27 11:44:16 -08001000 }
1001 }
1002
1003 @Override
1004 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
1005 if (super.performAccessibilityActionInternal(action, arguments)) {
1006 return true;
1007 }
Lyn Hane68d0912019-05-02 18:28:01 -07001008 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
1009
1010 // R constants are not final so we cannot use switch-case here.
1011 if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
1012 mBubbleData.dismissAll(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
1013 return true;
1014 } else if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
1015 mBubbleData.setExpanded(false);
1016 return true;
1017 } else if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
1018 mBubbleData.setExpanded(true);
1019 return true;
1020 } else if (action == R.id.action_move_top_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001021 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001022 return true;
1023 } else if (action == R.id.action_move_top_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001024 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.top);
Lyn Hane68d0912019-05-02 18:28:01 -07001025 return true;
1026 } else if (action == R.id.action_move_bottom_left) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001027 mStackAnimationController.springStackAfterFling(stackBounds.left, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001028 return true;
1029 } else if (action == R.id.action_move_bottom_right) {
Joshua Tsuji7155bf12020-02-13 16:14:29 -05001030 mStackAnimationController.springStackAfterFling(stackBounds.right, stackBounds.bottom);
Lyn Hane68d0912019-05-02 18:28:01 -07001031 return true;
Mady Mellor217b2e92019-02-27 11:44:16 -08001032 }
1033 return false;
1034 }
1035
Lyn Han6c40fe72019-05-08 14:06:33 -07001036 /**
1037 * Update content description for a11y TalkBack.
1038 */
1039 public void updateContentDescription() {
1040 if (mBubbleData.getBubbles().isEmpty()) {
1041 return;
1042 }
1043 Bubble topBubble = mBubbleData.getBubbles().get(0);
1044 String appName = topBubble.getAppName();
Ned Burns00b4b2d2019-10-17 22:09:27 -04001045 Notification notification = topBubble.getEntry().getSbn().getNotification();
Lyn Han6c40fe72019-05-08 14:06:33 -07001046 CharSequence titleCharSeq = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
Lyn Han93cd3132020-02-18 18:22:05 -08001047 String titleStr = getResources().getString(R.string.notification_bubble_title);
Lyn Han6c40fe72019-05-08 14:06:33 -07001048 if (titleCharSeq != null) {
1049 titleStr = titleCharSeq.toString();
1050 }
1051 int moreCount = mBubbleContainer.getChildCount() - 1;
1052
1053 // Example: Title from app name.
1054 String singleDescription = getResources().getString(
1055 R.string.bubble_content_description_single, titleStr, appName);
1056
1057 // Example: Title from app name and 4 more.
1058 String stackDescription = getResources().getString(
1059 R.string.bubble_content_description_stack, titleStr, appName, moreCount);
1060
1061 if (mIsExpanded) {
1062 // TODO(b/129522932) - update content description for each bubble in expanded view.
1063 } else {
1064 // Collapsed stack.
1065 if (moreCount > 0) {
1066 mBubbleContainer.setContentDescription(stackDescription);
1067 } else {
1068 mBubbleContainer.setContentDescription(singleDescription);
1069 }
1070 }
1071 }
1072
Mark Renouf821e6782019-04-01 14:17:37 -04001073 private void updateSystemGestureExcludeRects() {
1074 // Exclude the region occupied by the first BubbleView in the stack
1075 Rect excludeZone = mSystemGestureExclusionRects.get(0);
Lyn Hanc47e1712020-01-28 21:43:34 -08001076 if (getBubbleCount() > 0) {
Mark Renouf821e6782019-04-01 14:17:37 -04001077 View firstBubble = mBubbleContainer.getChildAt(0);
1078 excludeZone.set(firstBubble.getLeft(), firstBubble.getTop(), firstBubble.getRight(),
1079 firstBubble.getBottom());
1080 excludeZone.offset((int) (firstBubble.getTranslationX() + 0.5f),
1081 (int) (firstBubble.getTranslationY() + 0.5f));
1082 mBubbleContainer.setSystemGestureExclusionRects(mSystemGestureExclusionRects);
1083 } else {
1084 excludeZone.setEmpty();
1085 mBubbleContainer.setSystemGestureExclusionRects(Collections.emptyList());
1086 }
1087 }
1088
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001089 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -08001090 * Sets the listener to notify when the bubble stack is expanded.
1091 */
1092 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
1093 mExpandListener = listener;
1094 }
1095
1096 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001097 * Whether the stack of bubbles is expanded or not.
1098 */
1099 public boolean isExpanded() {
1100 return mIsExpanded;
1101 }
1102
1103 /**
Mady Mellor047e24e2019-08-05 11:35:40 -07001104 * Whether the stack of bubbles is animating to or from expansion.
1105 */
1106 public boolean isExpansionAnimating() {
1107 return mIsExpansionAnimating;
1108 }
1109
1110 /**
Mady Mellorb8aaf972019-11-26 10:28:00 -08001111 * The {@link BadgedImageView} that is expanded, null if one does not exist.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001112 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001113 View getExpandedBubbleView() {
Mady Mellored99c272019-06-13 15:58:30 -07001114 return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
Mady Mellor3dff9e62019-02-05 18:12:53 -08001115 }
1116
1117 /**
1118 * The {@link Bubble} that is expanded, null if one does not exist.
1119 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001120 @Nullable
Lyn Han9f66c3b2020-03-05 23:59:29 -08001121 BubbleViewProvider getExpandedBubble() {
1122 return mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001123 }
1124
Mark Renouf71a3af62019-04-08 15:02:54 -04001125 // via BubbleData.Listener
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001126 @SuppressLint("ClickableViewAccessibility")
Mark Renouf71a3af62019-04-08 15:02:54 -04001127 void addBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001128 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001129 Log.d(TAG, "addBubble: " + bubble);
1130 }
Joshua Tsujib35f5912019-07-24 16:15:21 -04001131
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001132 if (getBubbleCount() == 0 && mShouldShowUserEducation) {
1133 // Override the default stack position if we're showing user education.
1134 mStackAnimationController.setStackPosition(
1135 mStackAnimationController.getDefaultStartPosition());
1136 }
1137
Lyn Hanc47e1712020-01-28 21:43:34 -08001138 if (getBubbleCount() == 0) {
Joshua Tsujib35f5912019-07-24 16:15:21 -04001139 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
1140 }
1141
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001142 if (bubble.getIconView() == null) {
1143 return;
1144 }
1145
Joshua Tsujib35f5912019-07-24 16:15:21 -04001146 // Set the dot position to the opposite of the side the stack is resting on, since the stack
1147 // resting slightly off-screen would result in the dot also being off-screen.
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001148 bubble.getIconView().setDotPositionOnLeft(
Joshua Tsujib35f5912019-07-24 16:15:21 -04001149 !mStackOnLeftOrWillBe /* onLeft */, false /* animate */);
1150
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001151 bubble.getIconView().setOnClickListener(mBubbleClickListener);
1152 bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
1153
Mady Mellored99c272019-06-13 15:58:30 -07001154 mBubbleContainer.addView(bubble.getIconView(), 0,
Mark Renouf71a3af62019-04-08 15:02:54 -04001155 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellored99c272019-06-13 15:58:30 -07001156 ViewClippingUtil.setClippingDeactivated(bubble.getIconView(), true, mClippingParameters);
Mark Renoufba5ab512019-05-02 15:21:01 -04001157 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001158 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001159 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001160 }
1161
1162 // via BubbleData.Listener
1163 void removeBubble(Bubble bubble) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001164 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001165 Log.d(TAG, "removeBubble: " + bubble);
1166 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001167 // Remove it from the views
Lyn Hane1395572020-03-23 15:48:54 -07001168 for (int i = 0; i < getBubbleCount(); i++) {
1169 View v = mBubbleContainer.getChildAt(i);
1170 if (v instanceof BadgedImageView
1171 && ((BadgedImageView) v).getKey().equals(bubble.getKey())) {
1172 mBubbleContainer.removeViewAt(i);
Mady Mellore967e962020-03-26 17:36:44 -07001173 bubble.cleanupViews();
Lyn Hane1395572020-03-23 15:48:54 -07001174 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
1175 return;
1176 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001177 }
Lyn Hane1395572020-03-23 15:48:54 -07001178 Log.d(TAG, "was asked to remove Bubble, but didn't find the view! " + bubble);
Lyn Hanb58c7562020-01-07 14:29:20 -08001179 }
1180
1181 private void updateOverflowBtnVisibility(boolean apply) {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001182 if (!BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1183 return;
1184 }
Lyn Hanb58c7562020-01-07 14:29:20 -08001185 if (mIsExpanded) {
1186 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001187 Log.d(TAG, "Show overflow button.");
Lyn Hanb58c7562020-01-07 14:29:20 -08001188 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001189 mBubbleOverflow.setBtnVisible(VISIBLE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001190 if (apply) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001191 mExpandedAnimationController.expandFromStack(() -> {
1192 updatePointerPosition();
1193 } /* after */);
Lyn Hanb58c7562020-01-07 14:29:20 -08001194 }
1195 } else {
1196 if (DEBUG_BUBBLE_STACK_VIEW) {
1197 Log.d(TAG, "Collapsed. Hide overflow button.");
1198 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001199 mBubbleOverflow.setBtnVisible(GONE);
Lyn Hanb58c7562020-01-07 14:29:20 -08001200 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001201 }
1202
1203 // via BubbleData.Listener
1204 void updateBubble(Bubble bubble) {
Mark Renoufba5ab512019-05-02 15:21:01 -04001205 animateInFlyoutForBubble(bubble);
Mark Renouf71a3af62019-04-08 15:02:54 -04001206 requestUpdate();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001207 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mark Renouf71a3af62019-04-08 15:02:54 -04001208 }
1209
Mark Renoufba5ab512019-05-02 15:21:01 -04001210 public void updateBubbleOrder(List<Bubble> bubbles) {
1211 for (int i = 0; i < bubbles.size(); i++) {
1212 Bubble bubble = bubbles.get(i);
Mady Mellored99c272019-06-13 15:58:30 -07001213 mBubbleContainer.reorderView(bubble.getIconView(), i);
Mark Renoufba5ab512019-05-02 15:21:01 -04001214 }
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001215 updateBubbleZOrdersAndDotPosition(false /* animate */);
Lyn Hanf44562b2020-03-30 16:40:46 -07001216 updatePointerPosition();
Mark Renoufba5ab512019-05-02 15:21:01 -04001217 }
1218
Mady Melloredd4ee12019-01-18 10:45:11 -08001219 /**
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001220 * Changes the currently selected bubble. If the stack is already expanded, the newly selected
1221 * bubble will be shown immediately. This does not change the expanded state or change the
1222 * position of any bubble.
1223 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001224 // via BubbleData.Listener
Lyn Han3cd75d72020-02-15 19:10:12 -08001225 public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001226 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001227 Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
1228 }
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001229 if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
1230 return;
1231 }
Lyn Han3cd75d72020-02-15 19:10:12 -08001232 final BubbleViewProvider previouslySelected = mExpandedBubble;
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001233 mExpandedBubble = bubbleToSelect;
Issei Suzukicac2a502019-04-16 16:52:50 +02001234
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001235 if (mIsExpanded) {
1236 // Make the container of the expanded view transparent before removing the expanded view
1237 // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
1238 // expanded view becomes visible on the screen. See b/126856255
1239 mExpandedViewContainer.setAlpha(0.0f);
1240 mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
Lyn Han3cd75d72020-02-15 19:10:12 -08001241 previouslySelected.setContentVisibility(false);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001242 updateExpandedBubble();
1243 updatePointerPosition();
1244 requestUpdate();
Lyn Han3cd75d72020-02-15 19:10:12 -08001245
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001246 logBubbleEvent(previouslySelected,
1247 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
1248 logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor99a302602019-06-14 11:39:56 -07001249 notifyExpansionChanged(previouslySelected, false /* expanded */);
1250 notifyExpansionChanged(bubbleToSelect, true /* expanded */);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001251 });
1252 }
1253 }
1254
1255 /**
1256 * Changes the expanded state of the stack.
1257 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001258 * @param shouldExpand whether the bubble stack should appear expanded
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001259 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001260 // via BubbleData.Listener
1261 public void setExpanded(boolean shouldExpand) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001262 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001263 Log.d(TAG, "setExpanded: " + shouldExpand);
1264 }
Lyn Han285ad302019-05-29 19:01:39 -07001265 if (shouldExpand == mIsExpanded) {
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001266 return;
1267 }
Joshua Tsujibe60a582020-03-23 17:17:26 -04001268
1269 mSysUiState
1270 .setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
1271 .commitUpdate(mContext.getDisplayId());
1272
Lyn Han285ad302019-05-29 19:01:39 -07001273 if (mIsExpanded) {
1274 animateCollapse();
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001275 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001276 } else {
Lyn Han285ad302019-05-29 19:01:39 -07001277 animateExpansion();
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001278 // TODO: move next line to BubbleData
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001279 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
1280 logBubbleEvent(mExpandedBubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001281 }
Mady Mellor99a302602019-06-14 11:39:56 -07001282 notifyExpansionChanged(mExpandedBubble, mIsExpanded);
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001283 }
1284
1285 /**
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001286 * If necessary, shows the user education view for the bubble stack. This appears the first
1287 * time a user taps on a bubble.
1288 *
1289 * @return true if user education was shown, false otherwise.
1290 */
1291 private boolean maybeShowStackUserEducation() {
1292 if (mShouldShowUserEducation && mUserEducationView.getVisibility() != VISIBLE) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001293 mUserEducationView.setAlpha(0);
1294 mUserEducationView.setVisibility(VISIBLE);
1295 // Post so we have height of mUserEducationView
1296 mUserEducationView.post(() -> {
1297 final int viewHeight = mUserEducationView.getHeight();
1298 PointF stackPosition = mStackAnimationController.getDefaultStartPosition();
1299 final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2);
1300 mUserEducationView.setTranslationY(translationY);
1301 mUserEducationView.animate()
1302 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1303 .setInterpolator(FAST_OUT_SLOW_IN)
1304 .alpha(1);
1305 });
1306 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, true);
1307 return true;
1308 }
1309 return false;
1310 }
1311
1312 /**
1313 * If necessary, hides the user education view for the bubble stack.
1314 *
1315 * @param fromExpansion if true this indicates the hide is happening due to the bubble being
1316 * expanded, false if due to a touch outside of the bubble stack.
1317 */
1318 void hideStackUserEducation(boolean fromExpansion) {
1319 if (mShouldShowUserEducation
1320 && mUserEducationView.getVisibility() == VISIBLE
1321 && !mAnimatingEducationAway) {
1322 mAnimatingEducationAway = true;
1323 mUserEducationView.animate()
1324 .alpha(0)
1325 .setDuration(fromExpansion
1326 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1327 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1328 .withEndAction(() -> {
1329 mAnimatingEducationAway = false;
1330 mShouldShowUserEducation = shouldShowBubblesEducation();
1331 mUserEducationView.setVisibility(GONE);
1332 });
1333 }
1334 }
1335
1336 /**
1337 * If necessary, toggles the user education view for the manage button. This is shown when the
1338 * bubble stack is expanded for the first time.
1339 *
1340 * @param show whether the user education view should show or not.
1341 */
1342 void maybeShowManageEducation(boolean show) {
1343 if (mManageEducationView == null) {
1344 return;
1345 }
1346 if (show
1347 && mShouldShowManageEducation
1348 && mManageEducationView.getVisibility() != VISIBLE
1349 && mIsExpanded) {
1350 mManageEducationView.setAlpha(0);
1351 mManageEducationView.setVisibility(VISIBLE);
1352 mManageEducationView.post(() -> {
1353 final Rect position =
1354 mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
1355 final int viewHeight = mManageEducationView.getManageViewHeight();
1356 final int inset = getResources().getDimensionPixelSize(
1357 R.dimen.bubbles_manage_education_top_inset);
1358 mManageEducationView.bringToFront();
1359 mManageEducationView.setManageViewPosition(position.left,
1360 position.top - viewHeight + inset);
1361 mManageEducationView.setPointerPosition(position.centerX() - position.left);
1362 mManageEducationView.animate()
1363 .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
1364 .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
1365 });
1366 Prefs.putBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, true);
1367 } else if (!show
1368 && mManageEducationView.getVisibility() == VISIBLE
1369 && !mAnimatingManageEducationAway) {
1370 mManageEducationView.animate()
1371 .alpha(0)
1372 .setDuration(mIsExpansionAnimating
1373 ? ANIMATE_STACK_USER_EDUCATION_DURATION_SHORT
1374 : ANIMATE_STACK_USER_EDUCATION_DURATION)
1375 .withEndAction(() -> {
1376 mAnimatingManageEducationAway = false;
1377 mShouldShowManageEducation = shouldShowManageEducation();
1378 mManageEducationView.setVisibility(GONE);
1379 });
1380 }
1381 }
1382
1383 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001384 * Dismiss the stack of bubbles.
Lyn Han1b4f25e2019-06-11 13:56:34 -07001385 *
Mark Renouf71a3af62019-04-08 15:02:54 -04001386 * @deprecated
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001387 */
Mark Renouf71a3af62019-04-08 15:02:54 -04001388 @Deprecated
Mady Mellorc3d7d062019-03-28 16:13:05 -07001389 void stackDismissed(int reason) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001390 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001391 Log.d(TAG, "stackDismissed: reason=" + reason);
1392 }
Mark Renouf71a3af62019-04-08 15:02:54 -04001393 mBubbleData.dismissAll(reason);
Steven Wua254dab2019-01-29 11:30:39 -05001394 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001395 SysUiStatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001396 }
1397
1398 /**
Joshua Tsujif5c6a9c2020-02-25 17:47:59 -05001399 * @deprecated use {@link #setExpanded(boolean)} and
1400 * {@link BubbleData#setSelectedBubble(Bubble)}
Mark Renoufc6ab73d2019-04-09 16:42:22 -04001401 */
1402 @Deprecated
1403 @MainThread
Mady Mellor9801e852019-01-22 14:50:28 -08001404 void collapseStack(Runnable endRunnable) {
Issei Suzukia8d07312019-06-07 12:56:19 +02001405 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001406 Log.d(TAG, "collapseStack(endRunnable)");
1407 }
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001408 mBubbleData.setExpanded(false);
Mady Mellor9801e852019-01-22 14:50:28 -08001409 // TODO - use the runnable at end of animation
1410 endRunnable.run();
1411 }
1412
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001413 void showExpandedViewContents(int displayId) {
1414 if (mExpandedBubble != null
Mady Melloradd5c6a92020-03-31 17:22:48 -07001415 && mExpandedBubble.getExpandedView() != null
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001416 && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
1417 mExpandedBubble.setContentVisibility(true);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001418 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001419 }
1420
Lyn Han285ad302019-05-29 19:01:39 -07001421 private void beforeExpandedViewAnimation() {
1422 hideFlyoutImmediate();
1423 updateExpandedBubble();
1424 updateExpandedView();
1425 mIsExpansionAnimating = true;
1426 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001427
Lyn Han285ad302019-05-29 19:01:39 -07001428 private void afterExpandedViewAnimation() {
1429 updateExpandedView();
1430 mIsExpansionAnimating = false;
1431 requestUpdate();
1432 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001433
Lyn Han285ad302019-05-29 19:01:39 -07001434 private void animateCollapse() {
1435 mIsExpanded = false;
Lyn Han3cd75d72020-02-15 19:10:12 -08001436 final BubbleViewProvider previouslySelected = mExpandedBubble;
Lyn Han285ad302019-05-29 19:01:39 -07001437 beforeExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001438 maybeShowManageEducation(false);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001439
Lyn Hanb58c7562020-01-07 14:29:20 -08001440 if (DEBUG_BUBBLE_STACK_VIEW) {
1441 Log.d(TAG, "animateCollapse");
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001442 Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
Lyn Han9f66c3b2020-03-05 23:59:29 -08001443 mExpandedBubble));
Lyn Hanb58c7562020-01-07 14:29:20 -08001444 }
1445 updateOverflowBtnVisibility(/* apply */ false);
Lyn Han285ad302019-05-29 19:01:39 -07001446 mBubbleContainer.cancelAllAnimations();
1447 mExpandedAnimationController.collapseBackToStack(
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001448 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
1449 /* collapseTo */,
Lyn Han285ad302019-05-29 19:01:39 -07001450 () -> {
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001451 mBubbleContainer.setActiveController(mStackAnimationController);
Lyn Han285ad302019-05-29 19:01:39 -07001452 afterExpandedViewAnimation();
Lyn Han3cd75d72020-02-15 19:10:12 -08001453 previouslySelected.setContentVisibility(false);
Lyn Han285ad302019-05-29 19:01:39 -07001454 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001455
Lyn Han285ad302019-05-29 19:01:39 -07001456 mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
1457 mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
1458 mExpandedViewContainer.animate()
1459 .setDuration(100)
1460 .alpha(0f);
1461 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001462
Lyn Han285ad302019-05-29 19:01:39 -07001463 private void animateExpansion() {
1464 mIsExpanded = true;
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001465 hideStackUserEducation(true /* fromExpansion */);
Lyn Han285ad302019-05-29 19:01:39 -07001466 beforeExpandedViewAnimation();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001467
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001468 mBubbleContainer.setActiveController(mExpandedAnimationController);
Lyn Hanb58c7562020-01-07 14:29:20 -08001469 updateOverflowBtnVisibility(/* apply */ false);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001470 mExpandedAnimationController.expandFromStack(() -> {
1471 updatePointerPosition();
1472 afterExpandedViewAnimation();
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001473 maybeShowManageEducation(true);
Joshua Tsuji61b38f52019-05-31 16:20:22 -04001474 } /* after */);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001475
Lyn Han285ad302019-05-29 19:01:39 -07001476 mExpandedViewContainer.setTranslationX(getCollapsedX());
1477 mExpandedViewContainer.setTranslationY(getCollapsedY());
1478 mExpandedViewContainer.setAlpha(0f);
1479
1480 mExpandedViewXAnim.animateToFinalPosition(0f);
1481 mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
1482 mExpandedViewContainer.animate()
1483 .setDuration(100)
1484 .alpha(1f);
1485 }
1486
1487 private float getCollapsedX() {
1488 return mStackAnimationController.getStackPosition().x < getWidth() / 2
1489 ? -mExpandedAnimateXDistance
1490 : mExpandedAnimateXDistance;
1491 }
1492
1493 private float getCollapsedY() {
1494 return Math.min(mStackAnimationController.getStackPosition().y,
1495 mExpandedAnimateYDistance);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001496 }
1497
Lyn Han3cd75d72020-02-15 19:10:12 -08001498 private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
Mady Mellor99a302602019-06-14 11:39:56 -07001499 if (mExpandListener != null && bubble != null) {
1500 mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
Mady Mellor3f2efdb2018-11-21 11:30:45 -08001501 }
1502 }
1503
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001504 /** Return the BubbleView at the given index from the bubble container. */
Mady Mellorb8aaf972019-11-26 10:28:00 -08001505 public BadgedImageView getBubbleAt(int i) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001506 return getBubbleCount() > i
Mady Mellorb8aaf972019-11-26 10:28:00 -08001507 ? (BadgedImageView) mBubbleContainer.getChildAt(i)
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001508 : null;
1509 }
1510
Joshua Tsujia19515f2019-02-13 18:02:29 -05001511 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
1512 public void onImeVisibilityChanged(boolean visible, int height) {
Mady Mellordf611cf2019-08-21 17:28:49 -07001513 mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0);
Joshua Tsuji4b395912019-04-19 17:18:40 -04001514
Joshua Tsujiff6b0f22020-03-09 14:55:19 -04001515 if (!mIsExpanded && getBubbleCount() > 0) {
1516 final float stackDestinationY =
1517 mStackAnimationController.animateForImeVisibility(visible);
1518
1519 // How far the stack is animating due to IME, we'll just animate the flyout by that
1520 // much too.
1521 final float stackDy =
1522 stackDestinationY - mStackAnimationController.getStackPosition().y;
1523
1524 // If the flyout is visible, translate it along with the bubble stack.
1525 if (mFlyout.getVisibility() == VISIBLE) {
1526 PhysicsAnimator.getInstance(mFlyout)
1527 .spring(DynamicAnimation.TRANSLATION_Y,
1528 mFlyout.getTranslationY() + stackDy,
1529 FLYOUT_IME_ANIMATION_SPRING_CONFIG)
1530 .start();
1531 }
Joshua Tsujia19515f2019-02-13 18:02:29 -05001532 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001533 }
1534
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001535 /**
1536 * This method is called by {@link android.app.ActivityView} because the BubbleStackView has a
1537 * higher Z-index than the ActivityView (so that dragged-out bubbles are visible over the AV).
1538 * ActivityView is asking BubbleStackView to subtract the stack's bounds from the provided
1539 * touchable region, so that the ActivityView doesn't consume events meant for the stack. Due to
1540 * the special nature of ActivityView, it does not respect the standard
1541 * {@link #dispatchTouchEvent} and {@link #onInterceptTouchEvent} methods typically used for
1542 * this purpose.
1543 *
1544 * BubbleStackView is MATCH_PARENT, so that bubbles can be positioned via their translation
1545 * properties for performance reasons. This means that the default implementation of this method
1546 * subtracts the entirety of the screen from the ActivityView's touchable region, resulting in
1547 * it not receiving any touch events. This was previously addressed by returning false in the
1548 * stack's {@link View#canReceivePointerEvents()} method, but this precluded the use of any
1549 * touch handlers in the stack or its child views.
1550 *
1551 * To support touch handlers, we're overriding this method to leave the ActivityView's touchable
1552 * region alone. The only touchable part of the stack that can ever overlap the AV is a
1553 * dragged-out bubble that is animating back into the row of bubbles. It's not worth continually
1554 * updating the touchable region to allow users to grab a bubble while it completes its ~50ms
1555 * animation back to the bubble row.
1556 *
1557 * NOTE: Any future additions to the stack that obscure the ActivityView region will need their
1558 * bounds subtracted here in order to receive touch events.
1559 */
1560 @Override
1561 public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
1562
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001563 }
1564
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001565 /**
1566 * If you're here because you're not receiving touch events on a view that is a descendant of
1567 * BubbleStackView, and you think BSV is intercepting them - it's not! You need to subtract the
1568 * bounds of the view in question in {@link #subtractObscuredTouchableRegion}. The ActivityView
1569 * consumes all touch events within its bounds, even for views like the BubbleStackView that are
1570 * above it. It ignores typical view touch handling methods like this one and
1571 * dispatchTouchEvent.
1572 */
1573 @Override
1574 public boolean onInterceptTouchEvent(MotionEvent ev) {
1575 return super.onInterceptTouchEvent(ev);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001576 }
1577
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001578 @Override
1579 public boolean dispatchTouchEvent(MotionEvent ev) {
1580 boolean dispatched = super.dispatchTouchEvent(ev);
1581
1582 // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned
1583 // at the front of the stack (under the touch position). Subsequent ACTION_MOVE events will
1584 // then be passed to the new bubble, which will not consume them since it hasn't received an
1585 // ACTION_DOWN yet. Work around this by passing MotionEvents directly to the touch handler
1586 // until the current gesture ends with an ACTION_UP event.
1587 if (!dispatched && !mIsExpanded && mIsGestureInProgress) {
1588 dispatched = mBubbleTouchListener.onTouch(this /* view */, ev);
Joshua Tsuji442b6272019-02-08 13:23:43 -05001589 }
1590
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001591 mIsGestureInProgress =
1592 ev.getAction() != MotionEvent.ACTION_UP
1593 && ev.getAction() != MotionEvent.ACTION_CANCEL;
1594
1595 return dispatched;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001596 }
1597
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001598 void setFlyoutStateForDragLength(float deltaX) {
Joshua Tsuji8e05aab2019-08-22 14:57:50 -04001599 // This shouldn't happen, but if it does, just wait until the flyout lays out. This method
1600 // is continually called.
1601 if (mFlyout.getWidth() <= 0) {
1602 return;
1603 }
1604
Joshua Tsuji6549e702019-05-02 13:13:16 -04001605 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
1606 mFlyoutDragDeltaX = deltaX;
1607
1608 final float collapsePercent =
1609 onLeft ? -deltaX / mFlyout.getWidth() : deltaX / mFlyout.getWidth();
1610 mFlyout.setCollapsePercent(Math.min(1f, Math.max(0f, collapsePercent)));
1611
Lyn Han61d5d562019-07-01 17:39:38 -07001612 // Calculate how to translate the flyout if it has been dragged too far in either direction.
Joshua Tsuji6549e702019-05-02 13:13:16 -04001613 float overscrollTranslation = 0f;
1614 if (collapsePercent < 0f || collapsePercent > 1f) {
1615 // Whether we are more than 100% transitioned to the dot.
1616 final boolean overscrollingPastDot = collapsePercent > 1f;
1617
1618 // Whether we are overscrolling physically to the left - this can either be pulling the
1619 // flyout away from the stack (if the stack is on the right) or pushing it to the left
1620 // after it has already become the dot.
1621 final boolean overscrollingLeft =
1622 (onLeft && collapsePercent > 1f) || (!onLeft && collapsePercent < 0f);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001623 overscrollTranslation =
1624 (overscrollingPastDot ? collapsePercent - 1f : collapsePercent * -1)
1625 * (overscrollingLeft ? -1 : 1)
1626 * (mFlyout.getWidth() / (FLYOUT_OVERSCROLL_ATTENUATION_FACTOR
Lyn Han522e9ff2019-05-17 13:26:13 -07001627 // Attenuate the smaller dot less than the larger flyout.
1628 / (overscrollingPastDot ? 2 : 1)));
Joshua Tsuji6549e702019-05-02 13:13:16 -04001629 }
1630
1631 mFlyout.setTranslationX(mFlyout.getRestingTranslationX() + overscrollTranslation);
1632 }
1633
Joshua Tsuji20103542020-02-18 14:06:28 -05001634 /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001635 private boolean passEventToMagnetizedObject(MotionEvent event) {
Joshua Tsuji20103542020-02-18 14:06:28 -05001636 return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
1637 }
1638
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001639 /**
1640 * Dismisses the magnetized object - either an individual bubble, if we're expanded, or the
1641 * stack, if we're collapsed.
1642 */
1643 private void dismissMagnetizedObject() {
1644 if (mIsExpanded) {
1645 final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
1646 final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView);
1647
1648 if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) {
1649 mBubbleData.notificationEntryRemoved(
1650 draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
1651 }
1652 } else {
1653 mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
1654 }
1655 }
1656
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001657 /** Prepares and starts the desaturate/darken animation on the bubble stack. */
1658 private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
1659 mDesaturateAndDarkenTargetView = targetView;
1660
1661 if (desaturateAndDarken) {
1662 // Use the animated paint for the bubbles.
1663 mDesaturateAndDarkenTargetView.setLayerType(
1664 View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint);
1665 mDesaturateAndDarkenAnimator.removeAllListeners();
1666 mDesaturateAndDarkenAnimator.start();
1667 } else {
1668 mDesaturateAndDarkenAnimator.removeAllListeners();
1669 mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() {
1670 @Override
1671 public void onAnimationEnd(Animator animation) {
1672 super.onAnimationEnd(animation);
1673 // Stop using the animated paint.
1674 resetDesaturationAndDarken();
1675 }
1676 });
1677 mDesaturateAndDarkenAnimator.reverse();
1678 }
1679 }
1680
1681 private void resetDesaturationAndDarken() {
1682 mDesaturateAndDarkenAnimator.removeAllListeners();
1683 mDesaturateAndDarkenAnimator.cancel();
1684 mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
1685 }
1686
Lyn Han634483c2019-06-28 16:52:47 -07001687 /** Animates in the dismiss target. */
Joshua Tsuji7dd88b02020-03-27 17:43:09 -04001688 private void springInDismissTargetMaybe() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001689 if (mShowingDismiss) {
1690 return;
1691 }
1692
1693 mShowingDismiss = true;
1694
Joshua Tsuji20103542020-02-18 14:06:28 -05001695 mDismissTargetContainer.bringToFront();
1696 mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
1697 mDismissTargetContainer.setVisibility(VISIBLE);
1698
1699 mDismissTargetAnimator.cancel();
1700 mDismissTargetAnimator
1701 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
1702 .start();
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001703 }
1704
1705 /**
1706 * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they
1707 * were dragged into the target and encircled.
1708 */
Lyn Han634483c2019-06-28 16:52:47 -07001709 private void hideDismissTarget() {
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001710 if (!mShowingDismiss) {
1711 return;
1712 }
1713
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001714 mShowingDismiss = false;
Joshua Tsuji4accf5982019-04-22 17:36:11 -04001715
Joshua Tsuji20103542020-02-18 14:06:28 -05001716 mDismissTargetAnimator
1717 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
1718 mDismissTargetSpring)
1719 .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
1720 .start();
Joshua Tsuji19e22e4242019-04-17 13:29:10 -04001721 }
1722
Joshua Tsuji6549e702019-05-02 13:13:16 -04001723 /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
1724 private void animateFlyoutCollapsed(boolean collapsed, float velX) {
1725 final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001726 // If the flyout was tapped, we want a higher stiffness for the collapse animation so it's
1727 // faster.
1728 mFlyoutTransitionSpring.getSpring().setStiffness(
1729 (mBubbleToExpandAfterFlyoutCollapse != null)
1730 ? SpringForce.STIFFNESS_MEDIUM
1731 : SpringForce.STIFFNESS_LOW);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001732 mFlyoutTransitionSpring
1733 .setStartValue(mFlyoutDragDeltaX)
1734 .setStartVelocity(velX)
1735 .animateToFinalPosition(collapsed
1736 ? (onLeft ? -mFlyout.getWidth() : mFlyout.getWidth())
1737 : 0f);
1738 }
1739
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001740 /**
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001741 * Calculates the y position of the expanded view when it is expanded.
1742 */
Lyn Han285ad302019-05-29 19:01:39 -07001743 float getExpandedViewY() {
Lyn Han4a8efe32019-05-30 09:43:27 -07001744 return getStatusBarHeight() + mBubbleSize + mBubblePaddingTop + mPointerHeight;
Mady Mellor44ee2fe2019-01-30 17:51:16 -08001745 }
1746
1747 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001748 * Animates in the flyout for the given bubble, if available, and then hides it after some time.
1749 */
1750 @VisibleForTesting
1751 void animateInFlyoutForBubble(Bubble bubble) {
Mady Mellordf898fd2020-01-09 09:26:36 -08001752 Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001753 final BadgedImageView bubbleView = bubble.getIconView();
Mady Mellordf898fd2020-01-09 09:26:36 -08001754 if (flyoutMessage == null
1755 || flyoutMessage.message == null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001756 || !bubble.showFlyout()
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001757 || (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE)
Mark Renoufc19b4732019-06-26 12:08:33 -04001758 || isExpanded()
1759 || mIsExpansionAnimating
Joshua Tsuji14e68552019-06-06 17:17:08 -04001760 || mIsGestureInProgress
Lyn Hanf1f2c332019-08-23 17:06:56 -07001761 || mBubbleToExpandAfterFlyoutCollapse != null
Mady Mellorb8aaf972019-11-26 10:28:00 -08001762 || bubbleView == null) {
1763 if (bubbleView != null) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001764 bubbleView.removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001765 }
Joshua Tsuji14e68552019-06-06 17:17:08 -04001766 // Skip the message if none exists, we're expanded or animating expansion, or we're
Lyn Hanf1f2c332019-08-23 17:06:56 -07001767 // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
Mark Renoufc19b4732019-06-26 12:08:33 -04001768 return;
1769 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001770
Lyn Hanf1f2c332019-08-23 17:06:56 -07001771 mFlyoutDragDeltaX = 0f;
1772 clearFlyoutOnHide();
Mady Mellorb8aaf972019-11-26 10:28:00 -08001773 mAfterFlyoutHidden = () -> {
1774 // Null it out to ensure it runs once.
1775 mAfterFlyoutHidden = null;
1776
1777 if (mBubbleToExpandAfterFlyoutCollapse != null) {
1778 // User tapped on the flyout and we should expand
1779 mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
1780 mBubbleData.setExpanded(true);
1781 mBubbleToExpandAfterFlyoutCollapse = null;
Joshua Tsuji36b1b2c2019-04-18 16:27:35 -04001782 }
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001783
1784 // Stop suppressing the dot now that the flyout has morphed into the dot.
1785 bubbleView.removeDotSuppressionFlag(
1786 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Lyn Hanf1f2c332019-08-23 17:06:56 -07001787 };
1788 mFlyout.setVisibility(INVISIBLE);
Joshua Tsujidd4d9f92019-05-13 13:57:38 -04001789
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001790 // Suppress the dot when we are animating the flyout.
1791 bubbleView.addDotSuppressionFlag(
1792 BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001793
Lyn Hanf1f2c332019-08-23 17:06:56 -07001794 // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
1795 post(() -> {
1796 // An auto-expanding bubble could have been posted during the time it takes to
1797 // layout.
1798 if (isExpanded()) {
1799 return;
1800 }
1801 final Runnable expandFlyoutAfterDelay = () -> {
1802 mAnimateInFlyout = () -> {
1803 mFlyout.setVisibility(VISIBLE);
1804 mFlyoutDragDeltaX =
1805 mStackAnimationController.isStackOnLeftSide()
1806 ? -mFlyout.getWidth()
1807 : mFlyout.getWidth();
1808 animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
1809 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Joshua Tsuji14e68552019-06-06 17:17:08 -04001810 };
Lyn Hanf1f2c332019-08-23 17:06:56 -07001811 mFlyout.postDelayed(mAnimateInFlyout, 200);
1812 };
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001813
1814 if (bubble.getIconView() == null) {
1815 return;
1816 }
1817
Mady Mellordf898fd2020-01-09 09:26:36 -08001818 mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
1819 mStackAnimationController.getStackPosition(), getWidth(),
Lyn Hanf1f2c332019-08-23 17:06:56 -07001820 mStackAnimationController.isStackOnLeftSide(),
Mady Mellor05e860b2019-10-30 22:48:15 -07001821 bubble.getIconView().getDotColor() /* dotColor */,
Lyn Hanf1f2c332019-08-23 17:06:56 -07001822 expandFlyoutAfterDelay /* onLayoutComplete */,
Mady Mellorb8aaf972019-11-26 10:28:00 -08001823 mAfterFlyoutHidden,
1824 bubble.getIconView().getDotCenter(),
1825 !bubble.showDot());
Lyn Hanf1f2c332019-08-23 17:06:56 -07001826 mFlyout.bringToFront();
1827 });
Mark Renoufc19b4732019-06-26 12:08:33 -04001828 mFlyout.removeCallbacks(mHideFlyout);
1829 mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
Muhammad Qureshi9bced7d2020-01-16 15:22:12 -08001830 logBubbleEvent(bubble, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001831 }
1832
1833 /** Hide the flyout immediately and cancel any pending hide runnables. */
1834 private void hideFlyoutImmediate() {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001835 clearFlyoutOnHide();
Joshua Tsuji14e68552019-06-06 17:17:08 -04001836 mFlyout.removeCallbacks(mAnimateInFlyout);
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001837 mFlyout.removeCallbacks(mHideFlyout);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001838 mFlyout.hideFlyout();
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001839 }
1840
Lyn Hanf1f2c332019-08-23 17:06:56 -07001841 private void clearFlyoutOnHide() {
1842 mFlyout.removeCallbacks(mAnimateInFlyout);
Mady Mellorb8aaf972019-11-26 10:28:00 -08001843 if (mAfterFlyoutHidden == null) {
Lyn Hanf1f2c332019-08-23 17:06:56 -07001844 return;
1845 }
Mady Mellorb8aaf972019-11-26 10:28:00 -08001846 mAfterFlyoutHidden.run();
1847 mAfterFlyoutHidden = null;
Lyn Hanf1f2c332019-08-23 17:06:56 -07001848 }
1849
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001850 @Override
1851 public void getBoundsOnScreen(Rect outRect) {
Mady Mellor5a3e94b2020-02-07 12:16:21 -08001852 if (mUserEducationView != null && mUserEducationView.getVisibility() == VISIBLE) {
1853 // When user education shows then capture all touches
1854 outRect.set(0, 0, getWidth(), getHeight());
1855 return;
1856 }
1857
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001858 if (!mIsExpanded) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001859 if (getBubbleCount() > 0) {
Mady Mellor217b2e92019-02-27 11:44:16 -08001860 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
1861 }
Mady Mellore9371bc2019-07-10 18:50:59 -07001862 // Increase the touch target size of the bubble
1863 outRect.top -= mBubbleTouchPadding;
1864 outRect.left -= mBubbleTouchPadding;
1865 outRect.right += mBubbleTouchPadding;
1866 outRect.bottom += mBubbleTouchPadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001867 } else {
1868 mBubbleContainer.getBoundsOnScreen(outRect);
1869 }
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001870
Joshua Tsuji6549e702019-05-02 13:13:16 -04001871 if (mFlyout.getVisibility() == View.VISIBLE) {
Joshua Tsuji614b1df2019-03-26 13:57:05 -04001872 final Rect flyoutBounds = new Rect();
1873 mFlyout.getBoundsOnScreen(flyoutBounds);
1874 outRect.union(flyoutBounds);
1875 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001876 }
1877
1878 private int getStatusBarHeight() {
1879 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001880 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001881 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -05001882 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -05001883 insets.getDisplayCutout() != null
1884 ? insets.getDisplayCutout().getSafeInsetTop()
1885 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -08001886 }
1887
1888 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001889 }
1890
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001891 private void requestUpdate() {
Mady Mellorbc078c22019-03-26 17:10:34 -07001892 if (mViewUpdatedRequested || mIsExpansionAnimating) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001893 return;
1894 }
1895 mViewUpdatedRequested = true;
1896 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
1897 invalidate();
1898 }
1899
1900 private void updateExpandedBubble() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001901 if (DEBUG_BUBBLE_STACK_VIEW) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001902 Log.d(TAG, "updateExpandedBubble()");
1903 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001904 mExpandedViewContainer.removeAllViews();
Lyn Han3cd75d72020-02-15 19:10:12 -08001905 if (mIsExpanded && mExpandedBubble != null) {
1906 BubbleExpandedView bev = mExpandedBubble.getExpandedView();
Lyn Hana0bb02e2020-01-28 17:57:27 -08001907 mExpandedViewContainer.addView(bev);
1908 bev.populateExpandedView();
1909 mExpandedViewContainer.setVisibility(VISIBLE);
Issei Suzukic0387542019-03-08 17:31:14 +01001910 mExpandedViewContainer.setAlpha(1.0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001911 }
1912 }
1913
Lyn Han285ad302019-05-29 19:01:39 -07001914 private void updateExpandedView() {
Issei Suzukia8d07312019-06-07 12:56:19 +02001915 if (DEBUG_BUBBLE_STACK_VIEW) {
Lyn Han285ad302019-05-29 19:01:39 -07001916 Log.d(TAG, "updateExpandedView: mIsExpanded=" + mIsExpanded);
Mark Renouf9ba6cea2019-04-17 11:53:50 -04001917 }
Joshua Tsuji6549e702019-05-02 13:13:16 -04001918
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001919 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -08001920 if (mIsExpanded) {
Lyn Han285ad302019-05-29 19:01:39 -07001921 final float y = getExpandedViewY();
Mady Mellor5d8f1402019-02-21 18:23:52 -08001922 if (!mExpandedViewYAnim.isRunning()) {
1923 // We're not animating so set the value
1924 mExpandedViewContainer.setTranslationY(y);
Lyn Han3cd75d72020-02-15 19:10:12 -08001925 if (mExpandedBubble != null) {
Lyn Hanb58c7562020-01-07 14:29:20 -08001926 mExpandedBubble.getExpandedView().updateView();
1927 }
Mady Mellor5d8f1402019-02-21 18:23:52 -08001928 } else {
Mady Mellorbc078c22019-03-26 17:10:34 -07001929 // We are animating so update the value; there is an end listener on the animator
1930 // that will ensure expandedeView.updateView gets called.
Mady Mellor5d8f1402019-02-21 18:23:52 -08001931 mExpandedViewYAnim.animateToFinalPosition(y);
1932 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001933 }
Mady Mellor3dff9e62019-02-05 18:12:53 -08001934
Joshua Tsuji6549e702019-05-02 13:13:16 -04001935 mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001936 updateBubbleZOrdersAndDotPosition(false);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001937 }
1938
1939 /** Sets the appropriate Z-order and dot position for each bubble in the stack. */
Joshua Tsuji2862f2e2019-07-29 12:32:33 -04001940 private void updateBubbleZOrdersAndDotPosition(boolean animate) {
Lyn Hanc47e1712020-01-28 21:43:34 -08001941 int bubbleCount = getBubbleCount();
Lyn Han1b4f25e2019-06-11 13:56:34 -07001942 for (int i = 0; i < bubbleCount; i++) {
Mady Mellorb8aaf972019-11-26 10:28:00 -08001943 BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
Mady Mellor70958542019-09-24 17:12:46 -07001944 bv.setZ((mMaxBubbles * mBubbleElevation) - i);
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001945
Joshua Tsuji6549e702019-05-02 13:13:16 -04001946 // If the dot is on the left, and so is the stack, we need to change the dot position.
1947 if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
Joshua Tsuji2ed260e2020-03-26 14:26:01 -04001948 bv.setDotPositionOnLeft(!mStackOnLeftOrWillBe, animate);
1949 }
1950
1951 if (!mIsExpanded && i > 0) {
1952 // If we're collapsed and this bubble is behind other bubbles, suppress its dot.
1953 bv.addDotSuppressionFlag(
1954 BadgedImageView.SuppressionFlag.BEHIND_STACK);
1955 } else {
1956 bv.removeDotSuppressionFlag(
1957 BadgedImageView.SuppressionFlag.BEHIND_STACK);
Joshua Tsuji6549e702019-05-02 13:13:16 -04001958 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001959 }
1960 }
1961
Mady Mellorde2d4d22019-01-29 14:15:34 -08001962 private void updatePointerPosition() {
Lyn Han9f66c3b2020-03-05 23:59:29 -08001963 if (mExpandedBubble == null) {
Lyn Han522e9ff2019-05-17 13:26:13 -07001964 return;
Mady Mellorde2d4d22019-01-29 14:15:34 -08001965 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08001966 int index = getBubbleIndex(mExpandedBubble);
Lyn Hanf44562b2020-03-30 16:40:46 -07001967 if (index == -1) {
1968 return;
1969 }
Lyn Han522e9ff2019-05-17 13:26:13 -07001970 float bubbleLeftFromScreenLeft = mExpandedAnimationController.getBubbleLeft(index);
1971 float halfBubble = mBubbleSize / 2f;
Mady Mellor9be3bed2019-08-21 17:26:26 -07001972 float bubbleCenter = bubbleLeftFromScreenLeft + halfBubble;
1973 // Padding might be adjusted for insets, so get it directly from the view
1974 bubbleCenter -= mExpandedViewContainer.getPaddingLeft();
Lyn Han9f66c3b2020-03-05 23:59:29 -08001975 mExpandedBubble.getExpandedView().setPointerPosition(bubbleCenter);
Mady Mellorde2d4d22019-01-29 14:15:34 -08001976 }
1977
Steven Wua254dab2019-01-29 11:30:39 -05001978 /**
1979 * @return the number of bubbles in the stack view.
1980 */
Steven Wub00225b2019-02-08 14:27:42 -05001981 public int getBubbleCount() {
Lyn Han8cc4bf82020-03-05 16:34:37 -08001982 if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
1983 // Subtract 1 for the overflow button that is always in the bubble container.
1984 return mBubbleContainer.getChildCount() - 1;
1985 }
1986 return mBubbleContainer.getChildCount();
Steven Wua254dab2019-01-29 11:30:39 -05001987 }
1988
1989 /**
1990 * Finds the bubble index within the stack.
1991 *
Lyn Han3cd75d72020-02-15 19:10:12 -08001992 * @param provider the bubble view provider with the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -05001993 * @return the index of the bubble view within the bubble stack. The range of the position
1994 * is between 0 and the bubble count minus 1.
1995 */
Lyn Han3cd75d72020-02-15 19:10:12 -08001996 int getBubbleIndex(@Nullable BubbleViewProvider provider) {
Lyn Han9f66c3b2020-03-05 23:59:29 -08001997 if (provider == null) {
Steven Wua62cb6a2019-02-15 17:12:51 -05001998 return 0;
1999 }
Lyn Han9f66c3b2020-03-05 23:59:29 -08002000 return mBubbleContainer.indexOfChild(provider.getIconView());
Steven Wua254dab2019-01-29 11:30:39 -05002001 }
2002
2003 /**
2004 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
2005 */
Steven Wub00225b2019-02-08 14:27:42 -05002006 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002007 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -05002008 .setScale(4, RoundingMode.CEILING.HALF_UP)
2009 .floatValue();
2010 }
2011
2012 /**
2013 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
2014 */
Steven Wub00225b2019-02-08 14:27:42 -05002015 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -05002016 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -05002017 .setScale(4, RoundingMode.CEILING.HALF_UP)
2018 .floatValue();
2019 }
2020
Joshua Tsujia19515f2019-02-13 18:02:29 -05002021 public PointF getStackPosition() {
2022 return mStackAnimationController.getStackPosition();
2023 }
2024
Steven Wua254dab2019-01-29 11:30:39 -05002025 /**
2026 * Logs the bubble UI event.
2027 *
2028 * @param bubble the bubble that is being interacted on. Null value indicates that
2029 * the user interaction is not specific to one bubble.
2030 * @param action the user interaction enum.
2031 */
Lyn Han3cd75d72020-02-15 19:10:12 -08002032 private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
2033 if (bubble == null) {
2034 return;
Steven Wua254dab2019-01-29 11:30:39 -05002035 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002036 bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
2037 getNormalizedYPosition(), getBubbleIndex(bubble));
Steven Wua254dab2019-01-29 11:30:39 -05002038 }
Mark Renouf041d7262019-02-06 12:09:41 -05002039
2040 /**
2041 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
2042 * a back key down/up event pair is forwarded to the bubble Activity.
2043 */
2044 boolean performBackPressIfNeeded() {
Lyn Han3cd75d72020-02-15 19:10:12 -08002045 if (!isExpanded() || mExpandedBubble == null) {
Mark Renouf041d7262019-02-06 12:09:41 -05002046 return false;
2047 }
Lyn Han3cd75d72020-02-15 19:10:12 -08002048 return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
Mark Renouf041d7262019-02-06 12:09:41 -05002049 }
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002050
Mady Mellor5a3e94b2020-02-07 12:16:21 -08002051 /** Whether the educational view should appear for bubbles. **/
2052 private boolean shouldShowBubblesEducation() {
2053 return BubbleDebugConfig.forceShowUserEducation(getContext())
2054 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_EDUCATION, false);
2055 }
2056
2057 /** Whether the educational view should appear for the expanded view "manage" button. **/
2058 private boolean shouldShowManageEducation() {
2059 return BubbleDebugConfig.forceShowUserEducation(getContext())
2060 || !Prefs.getBoolean(getContext(), HAS_SEEN_BUBBLES_MANAGE_EDUCATION, false);
2061 }
2062
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002063 /** For debugging only */
2064 List<Bubble> getBubblesOnScreen() {
2065 List<Bubble> bubbles = new ArrayList<>();
Lyn Hanc47e1712020-01-28 21:43:34 -08002066 for (int i = 0; i < getBubbleCount(); i++) {
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002067 View child = mBubbleContainer.getChildAt(i);
Mady Mellorb8aaf972019-11-26 10:28:00 -08002068 if (child instanceof BadgedImageView) {
2069 String key = ((BadgedImageView) child).getKey();
Mark Renouf9ba6cea2019-04-17 11:53:50 -04002070 Bubble bubble = mBubbleData.getBubbleWithKey(key);
2071 bubbles.add(bubble);
2072 }
2073 }
2074 return bubbles;
2075 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08002076}