blob: ab6de5b70fe37d6afd2fb3b1819a169d876ad37e [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 Mellorc3d6f7d2018-11-07 09:36:56 -080022import android.content.Context;
23import android.content.res.Resources;
Joshua Tsuji580c0bf2019-01-28 13:28:21 -050024import android.graphics.Outline;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080025import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080026import android.graphics.PointF;
27import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080028import android.graphics.RectF;
Steven Wua254dab2019-01-29 11:30:39 -050029import android.service.notification.StatusBarNotification;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050030import android.util.Log;
Steven Wua254dab2019-01-29 11:30:39 -050031import android.util.StatsLog;
Mady Mellordea7ecf2018-12-10 15:47:40 -080032import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080033import android.view.MotionEvent;
34import android.view.View;
Joshua Tsuji580c0bf2019-01-28 13:28:21 -050035import android.view.ViewOutlineProvider;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050037import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080038import android.view.WindowManager;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080039import android.widget.FrameLayout;
40
Mark Renoufcecc77b2019-01-30 16:32:24 -050041import androidx.annotation.MainThread;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080042import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080043import androidx.dynamicanimation.animation.DynamicAnimation;
44import androidx.dynamicanimation.animation.SpringAnimation;
45import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080046
Mady Melloredd4ee12019-01-18 10:45:11 -080047import com.android.internal.annotations.VisibleForTesting;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080048import com.android.internal.widget.ViewClippingUtil;
49import com.android.systemui.R;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080050import com.android.systemui.bubbles.animation.ExpandedAnimationController;
51import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
52import com.android.systemui.bubbles.animation.StackAnimationController;
Ned Burnsf81c4c42019-01-07 14:10:43 -050053import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080054
Steven Wua254dab2019-01-29 11:30:39 -050055import java.math.BigDecimal;
56import java.math.RoundingMode;
57
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080058/**
59 * Renders bubbles in a stack and handles animating expanded and collapsed states.
60 */
Joshua Tsuji442b6272019-02-08 13:23:43 -050061public class BubbleStackView extends FrameLayout {
Mark Renouf89b1a4a2018-12-04 14:59:45 -050062 private static final String TAG = "BubbleStackView";
Joshua Tsujib1a796b2019-01-16 15:43:12 -080063
64 /**
65 * Friction applied to fling animations. Since the stack must land on one of the sides of the
66 * screen, we want less friction horizontally so that the stack has a better chance of making it
67 * to the side without needing a spring.
68 */
69 private static final float FLING_FRICTION_X = 1.15f;
70 private static final float FLING_FRICTION_Y = 1.5f;
71
72 /**
73 * Damping ratio to use for the stack spring animation used to spring the stack to its final
74 * position after a fling.
75 */
76 private static final float SPRING_DAMPING_RATIO = 0.85f;
77
78 /**
79 * Minimum fling velocity required to trigger moving the stack from one side of the screen to
80 * the other.
81 */
82 private static final float ESCAPE_VELOCITY = 750f;
83
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080084 private Point mDisplaySize;
85
Joshua Tsujib1a796b2019-01-16 15:43:12 -080086 private final SpringAnimation mExpandedViewXAnim;
87 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -080088 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080089
90 private PhysicsAnimationLayout mBubbleContainer;
91 private StackAnimationController mStackAnimationController;
92 private ExpandedAnimationController mExpandedAnimationController;
93
Mady Mellor3dff9e62019-02-05 18:12:53 -080094 private FrameLayout mExpandedViewContainer;
95
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080096
97 private int mBubbleSize;
98 private int mBubblePadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080099 private int mExpandedAnimateXDistance;
100 private int mExpandedAnimateYDistance;
Joshua Tsujif44347f2019-02-12 14:28:06 -0500101 private int mStatusBarHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -0800102 private int mPipDismissHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -0500103 private int mImeOffset;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800104
Mady Mellor3dff9e62019-02-05 18:12:53 -0800105 private Bubble mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800106 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800107
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800108 private BubbleTouchHandler mTouchHandler;
Mady Mellorcd9b1302018-11-06 18:08:04 -0800109 private BubbleController.BubbleExpandListener mExpandListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800110 private BubbleExpandedView.OnBubbleBlockedListener mBlockedListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800111
112 private boolean mViewUpdatedRequested = false;
113 private boolean mIsAnimating = false;
114
Mady Mellor3dff9e62019-02-05 18:12:53 -0800115 private LayoutInflater mInflater;
116
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800117 // Used for determining view / touch intersection
118 int[] mTempLoc = new int[2];
119 RectF mTempRect = new RectF();
120
121 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
122 new ViewTreeObserver.OnPreDrawListener() {
123 @Override
124 public boolean onPreDraw() {
125 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
126 applyCurrentState();
127 mViewUpdatedRequested = false;
128 return true;
129 }
130 };
131
132 private ViewClippingUtil.ClippingParameters mClippingParameters =
133 new ViewClippingUtil.ClippingParameters() {
134
135 @Override
136 public boolean shouldFinish(View view) {
137 return false;
138 }
139
140 @Override
141 public boolean isClippingEnablingAllowed(View view) {
142 return !mIsExpanded;
143 }
144 };
145
Mady Mellorcfd06c12019-02-13 14:32:12 -0800146 public BubbleStackView(Context context, BubbleData data) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800147 super(context);
148
Mady Mellorcfd06c12019-02-13 14:32:12 -0800149 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800150 mInflater = LayoutInflater.from(context);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500151 mTouchHandler = new BubbleTouchHandler(context, this);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800152 setOnTouchListener(mTouchHandler);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500153 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800154
155 Resources res = getResources();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800156 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800157 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800158 mExpandedAnimateXDistance =
159 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
160 mExpandedAnimateYDistance =
161 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500162 mStatusBarHeight =
163 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800164 mPipDismissHeight = mContext.getResources().getDimensionPixelSize(
165 R.dimen.pip_dismiss_gradient_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500166 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800167
168 mDisplaySize = new Point();
169 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
170 wm.getDefaultDisplay().getSize(mDisplaySize);
171
172 int padding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
173 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800174
175 mStackAnimationController = new StackAnimationController();
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800176 mExpandedAnimationController = new ExpandedAnimationController(mDisplaySize);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800177
178 mBubbleContainer = new PhysicsAnimationLayout(context);
179 mBubbleContainer.setMaxRenderedChildren(
180 getResources().getInteger(R.integer.bubbles_max_rendered));
181 mBubbleContainer.setController(mStackAnimationController);
182 mBubbleContainer.setElevation(elevation);
183 mBubbleContainer.setPadding(padding, 0, padding, 0);
184 mBubbleContainer.setClipChildren(false);
185 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
186
Mady Mellor3dff9e62019-02-05 18:12:53 -0800187 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800188 mExpandedViewContainer.setElevation(elevation);
189 mExpandedViewContainer.setPadding(padding, padding, padding, padding);
190 mExpandedViewContainer.setClipChildren(false);
191 addView(mExpandedViewContainer);
192
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800193 mExpandedViewXAnim =
194 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
195 mExpandedViewXAnim.setSpring(
196 new SpringForce()
197 .setStiffness(SpringForce.STIFFNESS_LOW)
198 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
199
200 mExpandedViewYAnim =
201 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
202 mExpandedViewYAnim.setSpring(
203 new SpringForce()
204 .setStiffness(SpringForce.STIFFNESS_LOW)
205 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800206
207 setClipChildren(false);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500208
209 mBubbleContainer.bringToFront();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800210 }
211
212 @Override
213 protected void onDetachedFromWindow() {
214 super.onDetachedFromWindow();
215 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
216 }
217
218 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800219 public boolean onInterceptTouchEvent(MotionEvent ev) {
220 float x = ev.getRawX();
221 float y = ev.getRawY();
222 // If we're expanded only intercept if the tap is outside of the widget container
223 if (mIsExpanded && isIntersecting(mExpandedViewContainer, x, y)) {
224 return false;
225 } else {
226 return isIntersecting(mBubbleContainer, x, y);
227 }
228 }
229
230 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800231 * Updates the visibility of the 'dot' indicating an update on the bubble.
232 * @param key the {@link NotificationEntry#key} associated with the bubble.
233 */
234 public void updateDotVisibility(String key) {
235 Bubble b = mBubbleData.getBubble(key);
236 if (b != null) {
237 b.iconView.updateDotVisibility();
238 }
239 }
240
241 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800242 * Sets the listener to notify when the bubble stack is expanded.
243 */
244 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
245 mExpandListener = listener;
246 }
247
248 /**
Mady Mellore8e07712019-01-23 12:45:33 -0800249 * Sets the listener to notify when a bubble is blocked.
250 */
Mady Mellor3d82e682019-02-05 13:34:48 -0800251 public void setOnBlockedListener(BubbleExpandedView.OnBubbleBlockedListener listener) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800252 mBlockedListener = listener;
253 for (Bubble b : mBubbleData.getBubbles()) {
254 b.expandedView.setOnBlockedListener(mBlockedListener);
255 }
Mady Mellore8e07712019-01-23 12:45:33 -0800256 }
257
258 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800259 * Whether the stack of bubbles is expanded or not.
260 */
261 public boolean isExpanded() {
262 return mIsExpanded;
263 }
264
265 /**
266 * The {@link BubbleView} that is expanded, null if one does not exist.
267 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800268 BubbleView getExpandedBubbleView() {
269 return mExpandedBubble != null ? mExpandedBubble.iconView : null;
270 }
271
272 /**
273 * The {@link Bubble} that is expanded, null if one does not exist.
274 */
275 Bubble getExpandedBubble() {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800276 return mExpandedBubble;
277 }
278
279 /**
280 * Sets the bubble that should be expanded and expands if needed.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800281 *
282 * @param key the {@link NotificationEntry#key} associated with the bubble to expand.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800283 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800284 void setExpandedBubble(String key) {
285 Bubble bubbleToExpand = mBubbleData.getBubble(key);
Mady Melloracb12152019-01-29 15:24:48 -0800286 if (mIsExpanded && !bubbleToExpand.equals(mExpandedBubble)) {
287 // Previously expanded, notify that this bubble is no longer expanded
Mady Mellor3dff9e62019-02-05 18:12:53 -0800288 notifyExpansionChanged(mExpandedBubble.entry, false /* expanded */);
Mady Melloracb12152019-01-29 15:24:48 -0800289 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800290 Bubble prevBubble = mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800291 mExpandedBubble = bubbleToExpand;
Mady Mellorde2d4d22019-01-29 14:15:34 -0800292 if (!mIsExpanded) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800293 // If we weren't previously expanded we should animate open.
294 animateExpansion(true /* expand */);
Steven Wua254dab2019-01-29 11:30:39 -0500295 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800296 } else {
Mady Mellorde2d4d22019-01-29 14:15:34 -0800297 // Otherwise just update the views
298 // TODO: probably animate / page to expanded one
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800299 updateExpandedBubble();
Mady Mellorde2d4d22019-01-29 14:15:34 -0800300 updatePointerPosition();
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800301 requestUpdate();
Steven Wua254dab2019-01-29 11:30:39 -0500302 logBubbleEvent(prevBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
303 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800304 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800305 mExpandedBubble.entry.setShowInShadeWhenBubble(false);
306 notifyExpansionChanged(mExpandedBubble.entry, true /* expanded */);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800307 }
308
309 /**
Mady Melloredd4ee12019-01-18 10:45:11 -0800310 * Sets the entry that should be expanded and expands if needed.
311 */
312 @VisibleForTesting
313 public void setExpandedBubble(NotificationEntry entry) {
314 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
315 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
316 if (entry.equals(bv.getEntry())) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800317 setExpandedBubble(entry.key);
Mady Melloredd4ee12019-01-18 10:45:11 -0800318 }
319 }
320 }
321
322 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800323 * Adds a bubble to the top of the stack.
324 *
Mady Mellor3dff9e62019-02-05 18:12:53 -0800325 * @param entry the notification to add to the stack of bubbles.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800326 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800327 public void addBubble(NotificationEntry entry) {
328 Bubble b = new Bubble(entry, mInflater, this /* stackView */, mBlockedListener);
329 mBubbleData.addBubble(b);
330
331 mBubbleContainer.addView(b.iconView, 0,
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800332 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellor3dff9e62019-02-05 18:12:53 -0800333 ViewClippingUtil.setClippingDeactivated(b.iconView, true, mClippingParameters);
334
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800335 requestUpdate();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800336 logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800337 }
338
339 /**
340 * Remove a bubble from the stack.
341 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800342 public void removeBubble(String key) {
343 Bubble b = mBubbleData.removeBubble(key);
344 if (b == null) {
345 return;
346 }
347 b.entry.setBubbleDismissed(true);
348
349 // Remove it from the views
350 int removedIndex = mBubbleContainer.indexOfChild(b.iconView);
Mady Mellor94d94a72019-03-05 18:16:59 -0800351 b.expandedView.cleanUpExpandedState();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800352 mBubbleContainer.removeView(b.iconView);
353
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800354 int bubbleCount = mBubbleContainer.getChildCount();
Mark Renouf658c6bc2019-01-30 10:26:54 -0500355 if (bubbleCount == 0) {
356 // If no bubbles remain, collapse the entire stack.
357 collapseStack();
358 return;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800359 } else if (b.equals(mExpandedBubble)) {
Mark Renouf658c6bc2019-01-30 10:26:54 -0500360 // Was the current bubble just removed?
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800361 // If we have other bubbles and are expanded go to the next one or previous
362 // if the bubble removed was last
363 int nextIndex = bubbleCount > removedIndex ? removedIndex : bubbleCount - 1;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800364 BubbleView expandedBubble = (BubbleView) mBubbleContainer.getChildAt(nextIndex);
Mark Renouf658c6bc2019-01-30 10:26:54 -0500365 if (mIsExpanded) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800366 setExpandedBubble(expandedBubble.getKey());
Mark Renouf658c6bc2019-01-30 10:26:54 -0500367 } else {
368 mExpandedBubble = null;
369 }
Mady Mellorcd9b1302018-11-06 18:08:04 -0800370 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800371 logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800372 }
373
374 /**
375 * Dismiss the stack of bubbles.
376 */
377 public void stackDismissed() {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800378 for (Bubble bubble : mBubbleData.getBubbles()) {
379 bubble.entry.setBubbleDismissed(true);
Mady Mellor94d94a72019-03-05 18:16:59 -0800380 bubble.expandedView.cleanUpExpandedState();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800381 }
382 mBubbleData.clear();
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800383 collapseStack();
384 mBubbleContainer.removeAllViews();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800385 mExpandedViewContainer.removeAllViews();
Steven Wua254dab2019-01-29 11:30:39 -0500386 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
387 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800388 }
389
390 /**
391 * Updates a bubble in the stack.
392 *
Mady Mellor3dff9e62019-02-05 18:12:53 -0800393 * @param entry the entry to update in the stack.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800394 * @param updatePosition whether this bubble should be moved to top of the stack.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800395 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800396 public void updateBubble(NotificationEntry entry, boolean updatePosition) {
397 Bubble b = mBubbleData.getBubble(entry.key);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800398 mBubbleData.updateBubble(entry.key, entry);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800399
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800400 if (updatePosition && !mIsExpanded) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800401 // If alerting it gets promoted to top of the stack.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800402 if (mBubbleContainer.indexOfChild(b.iconView) != 0) {
403 mBubbleContainer.moveViewTo(b.iconView, 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800404 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800405 requestUpdate();
406 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800407 if (mIsExpanded && entry.equals(mExpandedBubble.entry)) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800408 entry.setShowInShadeWhenBubble(false);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800409 requestUpdate();
410 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800411 logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800412 }
413
414 /**
415 * @return the view the touch event is on
416 */
417 @Nullable
418 public View getTargetView(MotionEvent event) {
419 float x = event.getRawX();
420 float y = event.getRawY();
421 if (mIsExpanded) {
422 if (isIntersecting(mBubbleContainer, x, y)) {
423 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
424 BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
425 if (isIntersecting(view, x, y)) {
426 return view;
427 }
428 }
429 } else if (isIntersecting(mExpandedViewContainer, x, y)) {
430 return mExpandedViewContainer;
431 }
432 // Outside parts of view we care about.
433 return null;
434 }
435 // If we're collapsed, the stack is always the target.
436 return this;
437 }
438
439 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800440 * Collapses the stack of bubbles.
Mark Renoufcecc77b2019-01-30 16:32:24 -0500441 * <p>
442 * Must be called from the main thread.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800443 */
Mark Renoufcecc77b2019-01-30 16:32:24 -0500444 @MainThread
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800445 public void collapseStack() {
446 if (mIsExpanded) {
447 // TODO: Save opened bubble & move it to top of stack
448 animateExpansion(false /* shouldExpand */);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800449 notifyExpansionChanged(mExpandedBubble.entry, mIsExpanded);
Steven Wua254dab2019-01-29 11:30:39 -0500450 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800451 }
452 }
453
Mady Mellor9801e852019-01-22 14:50:28 -0800454 void collapseStack(Runnable endRunnable) {
455 collapseStack();
456 // TODO - use the runnable at end of animation
457 endRunnable.run();
458 }
459
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800460 /**
Mady Mellor94d94a72019-03-05 18:16:59 -0800461 * Expands the stack of bubbles.
Mark Renoufcecc77b2019-01-30 16:32:24 -0500462 * <p>
463 * Must be called from the main thread.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800464 */
Mark Renoufcecc77b2019-01-30 16:32:24 -0500465 @MainThread
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800466 public void expandStack() {
467 if (!mIsExpanded) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800468 String expandedBubbleKey = getBubbleAt(0).getKey();
469 setExpandedBubble(expandedBubbleKey);
Steven Wua254dab2019-01-29 11:30:39 -0500470 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800471 }
472 }
473
474 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800475 * Tell the stack to animate to collapsed or expanded state.
476 */
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800477 private void animateExpansion(boolean shouldExpand) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800478 if (mIsExpanded != shouldExpand) {
479 mIsExpanded = shouldExpand;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800480 updateExpandedBubble();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800481 applyCurrentState();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800482
483 mIsAnimating = true;
484
485 Runnable updateAfter = () -> {
486 applyCurrentState();
487 mIsAnimating = false;
488 requestUpdate();
489 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800490
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800491 if (shouldExpand) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800492 mBubbleContainer.setController(mExpandedAnimationController);
493 mExpandedAnimationController.expandFromStack(
Joshua Tsuji442b6272019-02-08 13:23:43 -0500494 mStackAnimationController.getStackPosition(),
495 () -> {
496 updatePointerPosition();
497 updateAfter.run();
Mady Mellorde2d4d22019-01-29 14:15:34 -0800498 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800499 } else {
500 mBubbleContainer.cancelAllAnimations();
501 mExpandedAnimationController.collapseBackToStack(
502 () -> {
503 mBubbleContainer.setController(mStackAnimationController);
504 updateAfter.run();
505 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800506 }
507
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800508 final float xStart =
509 mStackAnimationController.getStackPosition().x < getWidth() / 2
510 ? -mExpandedAnimateXDistance
511 : mExpandedAnimateXDistance;
512
513 final float yStart = Math.min(
514 mStackAnimationController.getStackPosition().y,
515 mExpandedAnimateYDistance);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800516 final float yDest = getYPositionForExpandedView();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800517
518 if (shouldExpand) {
519 mExpandedViewContainer.setTranslationX(xStart);
520 mExpandedViewContainer.setTranslationY(yStart);
521 mExpandedViewContainer.setAlpha(0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800522 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800523
524 mExpandedViewXAnim.animateToFinalPosition(shouldExpand ? 0f : xStart);
525 mExpandedViewYAnim.animateToFinalPosition(shouldExpand ? yDest : yStart);
526 mExpandedViewContainer.animate()
527 .setDuration(100)
528 .alpha(shouldExpand ? 1f : 0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800529 }
530 }
531
532 /**
533 * The width of the collapsed stack of bubbles.
534 */
535 public int getStackWidth() {
536 return mBubblePadding * (mBubbleContainer.getChildCount() - 1)
537 + mBubbleSize + mBubbleContainer.getPaddingEnd()
538 + mBubbleContainer.getPaddingStart();
539 }
540
Mady Mellor3dff9e62019-02-05 18:12:53 -0800541 private void notifyExpansionChanged(NotificationEntry entry, boolean expanded) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800542 if (mExpandListener != null) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800543 mExpandListener.onBubbleExpandChanged(expanded, entry != null ? entry.key : null);
544 }
545 }
546
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800547 /** Return the BubbleView at the given index from the bubble container. */
548 public BubbleView getBubbleAt(int i) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800549 return mBubbleContainer.getChildCount() > i
550 ? (BubbleView) mBubbleContainer.getChildAt(i)
551 : null;
552 }
553
Joshua Tsujia19515f2019-02-13 18:02:29 -0500554 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
555 public void onImeVisibilityChanged(boolean visible, int height) {
556 if (!mIsExpanded) {
557 if (visible) {
558 mStackAnimationController.updateBoundsForVisibleImeAndAnimate(height + mImeOffset);
559 } else {
560 mStackAnimationController.updateBoundsForInvisibleImeAndAnimate();
561 }
562 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800563 }
564
565 /** Called when a drag operation on an individual bubble has started. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500566 public void onBubbleDragStart(View bubble) {
567 mExpandedAnimationController.prepareForBubbleDrag(bubble);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800568 }
569
570 /** Called with the coordinates to which an individual bubble has been dragged. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500571 public void onBubbleDragged(View bubble, float x, float y) {
572 if (!mIsExpanded || mIsAnimating) {
573 return;
574 }
575
576 mExpandedAnimationController.dragBubbleOut(bubble, x, y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800577 }
578
579 /** Called when a drag operation on an individual bubble has finished. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500580 public void onBubbleDragFinish(
581 View bubble, float x, float y, float velX, float velY, boolean dismissed) {
582 if (!mIsExpanded || mIsAnimating) {
583 return;
584 }
585
586 if (dismissed) {
587 mExpandedAnimationController.prepareForDismissalWithVelocity(bubble, velX, velY);
588 } else {
589 mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
590 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800591 }
592
593 void onDragStart() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500594 if (mIsExpanded || mIsAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800595 return;
596 }
597
598 mStackAnimationController.cancelStackPositionAnimations();
599 mBubbleContainer.setController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800600 }
601
602 void onDragged(float x, float y) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500603 if (mIsExpanded || mIsAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800604 return;
605 }
606
607 mStackAnimationController.moveFirstBubbleWithStackFollowing(x, y);
608 }
609
610 void onDragFinish(float x, float y, float velX, float velY) {
611 // TODO: Add fling to bottom to dismiss.
612
613 if (mIsExpanded || mIsAnimating) {
614 return;
615 }
616
617 final boolean stackOnLeftSide = x
618 - mBubbleContainer.getChildAt(0).getWidth() / 2
619 < mDisplaySize.x / 2;
620
621 final boolean stackShouldFlingLeft = stackOnLeftSide
622 ? velX < ESCAPE_VELOCITY
623 : velX < -ESCAPE_VELOCITY;
624
625 final RectF stackBounds = mStackAnimationController.getAllowableStackPositionRegion();
626
627 // Target X translation (either the left or right side of the screen).
628 final float destinationRelativeX = stackShouldFlingLeft
629 ? stackBounds.left : stackBounds.right;
630
631 // Minimum velocity required for the stack to make it to the side of the screen.
632 final float escapeVelocity = getMinXVelocity(
633 x,
634 destinationRelativeX,
635 FLING_FRICTION_X);
636
637 // Use the touch event's velocity if it's sufficient, otherwise use the minimum velocity so
638 // that it'll make it all the way to the side of the screen.
639 final float startXVelocity = stackShouldFlingLeft
640 ? Math.min(escapeVelocity, velX)
641 : Math.max(escapeVelocity, velX);
642
643 mStackAnimationController.flingThenSpringFirstBubbleWithStackFollowing(
644 DynamicAnimation.TRANSLATION_X,
645 startXVelocity,
646 FLING_FRICTION_X,
647 new SpringForce()
648 .setStiffness(SpringForce.STIFFNESS_LOW)
649 .setDampingRatio(SPRING_DAMPING_RATIO),
650 destinationRelativeX);
651
652 mStackAnimationController.flingThenSpringFirstBubbleWithStackFollowing(
653 DynamicAnimation.TRANSLATION_Y,
654 velY,
655 FLING_FRICTION_Y,
656 new SpringForce()
657 .setStiffness(SpringForce.STIFFNESS_LOW)
658 .setDampingRatio(SPRING_DAMPING_RATIO),
659 /* destination */ null);
Steven Wua254dab2019-01-29 11:30:39 -0500660
661 logBubbleEvent(null /* no bubble associated with bubble stack move */,
662 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800663 }
664
665 /**
Mady Mellorfe7ec032019-01-30 17:32:49 -0800666 * Calculates how large the expanded view of the bubble can be. This takes into account the
667 * y position when the bubbles are expanded as well as the bounds of the dismiss target.
668 */
669 int getMaxExpandedHeight() {
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800670 boolean showOnTop = BubbleController.showBubblesAtTop(getContext());
Mady Mellorfe7ec032019-01-30 17:32:49 -0800671 int expandedY = (int) mExpandedAnimationController.getExpandedY();
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800672 if (showOnTop) {
673 // PIP dismiss view uses FLAG_LAYOUT_IN_SCREEN so we need to subtract the bottom inset
674 int pipDismissHeight = mPipDismissHeight - getBottomInset();
675 return mDisplaySize.y - expandedY - mBubbleSize - pipDismissHeight;
676 } else {
677 return expandedY - getStatusBarHeight();
678 }
679 }
680
681 /**
682 * Calculates the y position of the expanded view when it is expanded.
683 */
684 float getYPositionForExpandedView() {
685 boolean showOnTop = BubbleController.showBubblesAtTop(getContext());
686 if (showOnTop) {
687 return getStatusBarHeight() + mBubbleSize + mBubblePadding;
688 } else {
689 return mExpandedAnimationController.getExpandedY()
690 - mExpandedBubble.expandedView.getExpandedSize() - mBubblePadding;
691 }
692 }
693
694 /**
695 * Called when the height of the currently expanded view has changed (not via an
696 * update to the bubble's desired height but for some other reason, e.g. permission view
697 * goes away).
698 */
699 void onExpandedHeightChanged() {
700 if (mIsExpanded) {
701 requestUpdate();
702 }
Mady Mellorfe7ec032019-01-30 17:32:49 -0800703 }
704
705 /**
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800706 * Minimum velocity, in pixels/second, required to get from x to destX while being slowed by a
707 * given frictional force.
708 *
709 * This is not derived using real math, I just made it up because the math in FlingAnimation
710 * looks hard and this seems to work. It doesn't actually matter because if it doesn't make it
711 * to the edge via Fling, it'll get Spring'd there anyway.
712 *
713 * TODO(tsuji, or someone who likes math): Figure out math.
714 */
715 private float getMinXVelocity(float x, float destX, float friction) {
716 return (destX - x) * (friction * 5) + ESCAPE_VELOCITY;
717 }
718
719 @Override
720 public void getBoundsOnScreen(Rect outRect) {
721 if (!mIsExpanded) {
722 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
723 } else {
724 mBubbleContainer.getBoundsOnScreen(outRect);
725 }
726 }
727
728 private int getStatusBarHeight() {
729 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500730 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800731 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500732 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500733 insets.getDisplayCutout() != null
734 ? insets.getDisplayCutout().getSafeInsetTop()
735 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800736 }
737
738 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800739 }
740
Mady Mellorfe7ec032019-01-30 17:32:49 -0800741 private int getBottomInset() {
742 if (getRootWindowInsets() != null) {
743 WindowInsets insets = getRootWindowInsets();
744 return insets.getSystemWindowInsetBottom();
745 }
746 return 0;
747 }
748
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800749 private boolean isIntersecting(View view, float x, float y) {
750 mTempLoc = view.getLocationOnScreen();
751 mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
752 mTempLoc[1] + view.getHeight());
753 return mTempRect.contains(x, y);
754 }
755
756 private void requestUpdate() {
757 if (mViewUpdatedRequested || mIsAnimating) {
758 return;
759 }
760 mViewUpdatedRequested = true;
761 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
762 invalidate();
763 }
764
765 private void updateExpandedBubble() {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800766 mExpandedViewContainer.removeAllViews();
767 if (mExpandedBubble != null && mIsExpanded) {
768 mExpandedViewContainer.addView(mExpandedBubble.expandedView);
Mady Mellor5029fa62019-03-05 12:16:21 -0800769 mExpandedBubble.expandedView.populateExpandedView();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800770 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800771 }
772 }
773
774 private void applyCurrentState() {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500775 Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded);
776
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800777 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800778 if (mIsExpanded) {
Mady Mellor5029fa62019-03-05 12:16:21 -0800779 // First update the view so that it calculates a new height (ensuring the y position
780 // calculation is correct)
781 mExpandedBubble.expandedView.updateView();
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800782 final float y = getYPositionForExpandedView();
783 mExpandedViewContainer.setTranslationY(y);
Mady Mellor5029fa62019-03-05 12:16:21 -0800784 // Then update the view so that ActivityView knows we translated
Mady Mellor3dff9e62019-02-05 18:12:53 -0800785 mExpandedBubble.expandedView.updateView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800786 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800787
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800788 int bubbsCount = mBubbleContainer.getChildCount();
789 for (int i = 0; i < bubbsCount; i++) {
790 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800791 bv.updateDotVisibility();
792 bv.setZ(bubbsCount - i);
Joshua Tsuji580c0bf2019-01-28 13:28:21 -0500793
794 // Draw the shadow around the circle inscribed within the bubble's bounds. This
795 // (intentionally) does not draw a shadow behind the update dot, which should be drawing
796 // its own shadow since it's on a different (higher) plane.
797 bv.setOutlineProvider(new ViewOutlineProvider() {
798 @Override
799 public void getOutline(View view, Outline outline) {
800 outline.setOval(0, 0, mBubbleSize, mBubbleSize);
801 }
802 });
803 bv.setClipToOutline(false);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800804 }
805 }
806
Mady Mellorde2d4d22019-01-29 14:15:34 -0800807 private void updatePointerPosition() {
808 if (mExpandedBubble != null) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500809 float pointerPosition = mExpandedBubble.iconView.getTranslationX()
Mady Mellor3dff9e62019-02-05 18:12:53 -0800810 + (mExpandedBubble.iconView.getWidth() / 2f);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500811 mExpandedBubble.expandedView.setPointerPosition((int) pointerPosition);
Mady Mellorde2d4d22019-01-29 14:15:34 -0800812 }
813 }
814
Steven Wua254dab2019-01-29 11:30:39 -0500815 /**
816 * @return the number of bubbles in the stack view.
817 */
Steven Wub00225b2019-02-08 14:27:42 -0500818 public int getBubbleCount() {
Steven Wua254dab2019-01-29 11:30:39 -0500819 return mBubbleContainer.getChildCount();
820 }
821
822 /**
823 * Finds the bubble index within the stack.
824 *
Mady Mellor3dff9e62019-02-05 18:12:53 -0800825 * @param bubble the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -0500826 * @return the index of the bubble view within the bubble stack. The range of the position
827 * is between 0 and the bubble count minus 1.
828 */
Steven Wua62cb6a2019-02-15 17:12:51 -0500829 int getBubbleIndex(@Nullable Bubble bubble) {
830 if (bubble == null) {
831 return 0;
832 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800833 return mBubbleContainer.indexOfChild(bubble.iconView);
Steven Wua254dab2019-01-29 11:30:39 -0500834 }
835
836 /**
837 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
838 */
Steven Wub00225b2019-02-08 14:27:42 -0500839 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500840 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -0500841 .setScale(4, RoundingMode.CEILING.HALF_UP)
842 .floatValue();
843 }
844
845 /**
846 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
847 */
Steven Wub00225b2019-02-08 14:27:42 -0500848 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500849 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -0500850 .setScale(4, RoundingMode.CEILING.HALF_UP)
851 .floatValue();
852 }
853
Joshua Tsujia19515f2019-02-13 18:02:29 -0500854 public PointF getStackPosition() {
855 return mStackAnimationController.getStackPosition();
856 }
857
Steven Wua254dab2019-01-29 11:30:39 -0500858 /**
859 * Logs the bubble UI event.
860 *
861 * @param bubble the bubble that is being interacted on. Null value indicates that
862 * the user interaction is not specific to one bubble.
863 * @param action the user interaction enum.
864 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800865 private void logBubbleEvent(@Nullable Bubble bubble, int action) {
Steven Wua254dab2019-01-29 11:30:39 -0500866 if (bubble == null) {
867 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
868 null /* package name */,
869 null /* notification channel */,
870 0 /* notification ID */,
871 0 /* bubble position */,
872 getBubbleCount(),
873 action,
874 getNormalizedXPosition(),
875 getNormalizedYPosition());
876 } else {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800877 StatusBarNotification notification = bubble.entry.notification;
Steven Wua254dab2019-01-29 11:30:39 -0500878 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
879 notification.getPackageName(),
880 notification.getNotification().getChannelId(),
881 notification.getId(),
882 getBubbleIndex(bubble),
883 getBubbleCount(),
884 action,
885 getNormalizedXPosition(),
886 getNormalizedYPosition());
887 }
888 }
Mark Renouf041d7262019-02-06 12:09:41 -0500889
890 /**
891 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
892 * a back key down/up event pair is forwarded to the bubble Activity.
893 */
894 boolean performBackPressIfNeeded() {
895 if (!isExpanded()) {
896 return false;
897 }
898 return mExpandedBubble.expandedView.performBackPressIfNeeded();
899 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800900}