blob: 888e3fe282a8b16bf14f12120144c4ba6aca0e7a [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
Mark Renouf08bc42a2019-03-07 13:01:59 -050022import android.app.Notification;
23import android.app.PendingIntent;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080024import android.content.Context;
25import android.content.res.Resources;
Joshua Tsuji580c0bf2019-01-28 13:28:21 -050026import android.graphics.Outline;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080027import android.graphics.Point;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080028import android.graphics.PointF;
29import android.graphics.Rect;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080030import android.graphics.RectF;
Mady Mellor217b2e92019-02-27 11:44:16 -080031import android.os.Bundle;
Steven Wua254dab2019-01-29 11:30:39 -050032import android.service.notification.StatusBarNotification;
Mark Renouf89b1a4a2018-12-04 14:59:45 -050033import android.util.Log;
Steven Wua254dab2019-01-29 11:30:39 -050034import android.util.StatsLog;
Mady Mellordea7ecf2018-12-10 15:47:40 -080035import android.view.LayoutInflater;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036import android.view.MotionEvent;
37import android.view.View;
Joshua Tsuji580c0bf2019-01-28 13:28:21 -050038import android.view.ViewOutlineProvider;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080039import android.view.ViewTreeObserver;
Joshua Tsuji0fee7682019-01-25 11:37:49 -050040import android.view.WindowInsets;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080041import android.view.WindowManager;
Mady Mellor217b2e92019-02-27 11:44:16 -080042import android.view.accessibility.AccessibilityNodeInfo;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080043import android.widget.FrameLayout;
44
Mark Renoufcecc77b2019-01-30 16:32:24 -050045import androidx.annotation.MainThread;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080046import androidx.annotation.Nullable;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080047import androidx.dynamicanimation.animation.DynamicAnimation;
48import androidx.dynamicanimation.animation.SpringAnimation;
49import androidx.dynamicanimation.animation.SpringForce;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080050
Mady Melloredd4ee12019-01-18 10:45:11 -080051import com.android.internal.annotations.VisibleForTesting;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080052import com.android.internal.widget.ViewClippingUtil;
53import com.android.systemui.R;
Mark Renouf08bc42a2019-03-07 13:01:59 -050054import com.android.systemui.bubbles.BubbleController.DismissReason;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080055import com.android.systemui.bubbles.animation.ExpandedAnimationController;
56import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
57import com.android.systemui.bubbles.animation.StackAnimationController;
Ned Burnsf81c4c42019-01-07 14:10:43 -050058import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080059
Steven Wua254dab2019-01-29 11:30:39 -050060import java.math.BigDecimal;
61import java.math.RoundingMode;
62
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080063/**
64 * Renders bubbles in a stack and handles animating expanded and collapsed states.
65 */
Joshua Tsuji442b6272019-02-08 13:23:43 -050066public class BubbleStackView extends FrameLayout {
Mark Renouf89b1a4a2018-12-04 14:59:45 -050067 private static final String TAG = "BubbleStackView";
Mark Renouf08bc42a2019-03-07 13:01:59 -050068 private static final boolean DEBUG = false;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080069
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080070 private Point mDisplaySize;
71
Joshua Tsujib1a796b2019-01-16 15:43:12 -080072 private final SpringAnimation mExpandedViewXAnim;
73 private final SpringAnimation mExpandedViewYAnim;
Mady Mellorcfd06c12019-02-13 14:32:12 -080074 private final BubbleData mBubbleData;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080075
76 private PhysicsAnimationLayout mBubbleContainer;
77 private StackAnimationController mStackAnimationController;
78 private ExpandedAnimationController mExpandedAnimationController;
79
Mady Mellor3dff9e62019-02-05 18:12:53 -080080 private FrameLayout mExpandedViewContainer;
81
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080082
83 private int mBubbleSize;
84 private int mBubblePadding;
Joshua Tsujib1a796b2019-01-16 15:43:12 -080085 private int mExpandedAnimateXDistance;
86 private int mExpandedAnimateYDistance;
Joshua Tsujif44347f2019-02-12 14:28:06 -050087 private int mStatusBarHeight;
Mady Mellorfe7ec032019-01-30 17:32:49 -080088 private int mPipDismissHeight;
Joshua Tsujia19515f2019-02-13 18:02:29 -050089 private int mImeOffset;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080090
Mady Mellor3dff9e62019-02-05 18:12:53 -080091 private Bubble mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080092 private boolean mIsExpanded;
Mady Mellor3dff9e62019-02-05 18:12:53 -080093
Mady Mellor3f2efdb2018-11-21 11:30:45 -080094 private BubbleTouchHandler mTouchHandler;
Mady Mellorcd9b1302018-11-06 18:08:04 -080095 private BubbleController.BubbleExpandListener mExpandListener;
Mady Mellor3dff9e62019-02-05 18:12:53 -080096 private BubbleExpandedView.OnBubbleBlockedListener mBlockedListener;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080097
98 private boolean mViewUpdatedRequested = false;
99 private boolean mIsAnimating = false;
100
Mady Mellor3dff9e62019-02-05 18:12:53 -0800101 private LayoutInflater mInflater;
102
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800103 // Used for determining view / touch intersection
104 int[] mTempLoc = new int[2];
105 RectF mTempRect = new RectF();
106
107 private ViewTreeObserver.OnPreDrawListener mViewUpdater =
108 new ViewTreeObserver.OnPreDrawListener() {
109 @Override
110 public boolean onPreDraw() {
111 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
112 applyCurrentState();
113 mViewUpdatedRequested = false;
114 return true;
115 }
116 };
117
118 private ViewClippingUtil.ClippingParameters mClippingParameters =
119 new ViewClippingUtil.ClippingParameters() {
120
121 @Override
122 public boolean shouldFinish(View view) {
123 return false;
124 }
125
126 @Override
127 public boolean isClippingEnablingAllowed(View view) {
128 return !mIsExpanded;
129 }
130 };
131
Mady Mellorcfd06c12019-02-13 14:32:12 -0800132 public BubbleStackView(Context context, BubbleData data) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800133 super(context);
134
Mady Mellorcfd06c12019-02-13 14:32:12 -0800135 mBubbleData = data;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800136 mInflater = LayoutInflater.from(context);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500137 mTouchHandler = new BubbleTouchHandler(context, this);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800138 setOnTouchListener(mTouchHandler);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500139 mInflater = LayoutInflater.from(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800140
141 Resources res = getResources();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800142 mBubbleSize = res.getDimensionPixelSize(R.dimen.individual_bubble_size);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800143 mBubblePadding = res.getDimensionPixelSize(R.dimen.bubble_padding);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800144 mExpandedAnimateXDistance =
145 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
146 mExpandedAnimateYDistance =
147 res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
Joshua Tsujif44347f2019-02-12 14:28:06 -0500148 mStatusBarHeight =
149 res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800150 mPipDismissHeight = mContext.getResources().getDimensionPixelSize(
151 R.dimen.pip_dismiss_gradient_height);
Joshua Tsujia19515f2019-02-13 18:02:29 -0500152 mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800153
154 mDisplaySize = new Point();
155 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
156 wm.getDefaultDisplay().getSize(mDisplaySize);
157
158 int padding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
159 int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800160
161 mStackAnimationController = new StackAnimationController();
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800162 mExpandedAnimationController = new ExpandedAnimationController(mDisplaySize);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800163
164 mBubbleContainer = new PhysicsAnimationLayout(context);
165 mBubbleContainer.setMaxRenderedChildren(
166 getResources().getInteger(R.integer.bubbles_max_rendered));
167 mBubbleContainer.setController(mStackAnimationController);
168 mBubbleContainer.setElevation(elevation);
169 mBubbleContainer.setPadding(padding, 0, padding, 0);
170 mBubbleContainer.setClipChildren(false);
171 addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
172
Mady Mellor3dff9e62019-02-05 18:12:53 -0800173 mExpandedViewContainer = new FrameLayout(context);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800174 mExpandedViewContainer.setElevation(elevation);
175 mExpandedViewContainer.setPadding(padding, padding, padding, padding);
176 mExpandedViewContainer.setClipChildren(false);
177 addView(mExpandedViewContainer);
178
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800179 mExpandedViewXAnim =
180 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
181 mExpandedViewXAnim.setSpring(
182 new SpringForce()
183 .setStiffness(SpringForce.STIFFNESS_LOW)
184 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
185
186 mExpandedViewYAnim =
187 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
188 mExpandedViewYAnim.setSpring(
189 new SpringForce()
190 .setStiffness(SpringForce.STIFFNESS_LOW)
191 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800192
193 setClipChildren(false);
Mady Mellor217b2e92019-02-27 11:44:16 -0800194 setFocusable(true);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500195 mBubbleContainer.bringToFront();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800196 }
197
198 @Override
Mady Mellor217b2e92019-02-27 11:44:16 -0800199 public void getBoundsOnScreen(Rect outRect, boolean clipToParent) {
200 getBoundsOnScreen(outRect);
201 }
202
203 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800204 protected void onDetachedFromWindow() {
205 super.onDetachedFromWindow();
206 getViewTreeObserver().removeOnPreDrawListener(mViewUpdater);
207 }
208
209 @Override
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800210 public boolean onInterceptTouchEvent(MotionEvent ev) {
211 float x = ev.getRawX();
212 float y = ev.getRawY();
213 // If we're expanded only intercept if the tap is outside of the widget container
214 if (mIsExpanded && isIntersecting(mExpandedViewContainer, x, y)) {
215 return false;
216 } else {
217 return isIntersecting(mBubbleContainer, x, y);
218 }
219 }
220
Mady Mellor217b2e92019-02-27 11:44:16 -0800221 @Override
222 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
223 super.onInitializeAccessibilityNodeInfoInternal(info);
224 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
225 if (mIsExpanded) {
226 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
227 } else {
228 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
229 }
230 }
231
232 @Override
233 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
234 if (super.performAccessibilityActionInternal(action, arguments)) {
235 return true;
236 }
237 switch (action) {
238 case AccessibilityNodeInfo.ACTION_DISMISS:
Mark Renouf08bc42a2019-03-07 13:01:59 -0500239 stackDismissed(BubbleController.DISMISS_ACCESSIBILITY_ACTION);
Mady Mellor217b2e92019-02-27 11:44:16 -0800240 return true;
241 case AccessibilityNodeInfo.ACTION_COLLAPSE:
242 collapseStack();
243 return true;
244 case AccessibilityNodeInfo.ACTION_EXPAND:
245 expandStack();
246 return true;
247 }
248 return false;
249 }
250
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800251 /**
Mady Mellor3dff9e62019-02-05 18:12:53 -0800252 * Updates the visibility of the 'dot' indicating an update on the bubble.
253 * @param key the {@link NotificationEntry#key} associated with the bubble.
254 */
255 public void updateDotVisibility(String key) {
256 Bubble b = mBubbleData.getBubble(key);
257 if (b != null) {
258 b.iconView.updateDotVisibility();
259 }
260 }
261
262 /**
Mady Mellorcd9b1302018-11-06 18:08:04 -0800263 * Sets the listener to notify when the bubble stack is expanded.
264 */
265 public void setExpandListener(BubbleController.BubbleExpandListener listener) {
266 mExpandListener = listener;
267 }
268
269 /**
Mady Mellore8e07712019-01-23 12:45:33 -0800270 * Sets the listener to notify when a bubble is blocked.
271 */
Mady Mellor3d82e682019-02-05 13:34:48 -0800272 public void setOnBlockedListener(BubbleExpandedView.OnBubbleBlockedListener listener) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800273 mBlockedListener = listener;
274 for (Bubble b : mBubbleData.getBubbles()) {
275 b.expandedView.setOnBlockedListener(mBlockedListener);
276 }
Mady Mellore8e07712019-01-23 12:45:33 -0800277 }
278
279 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800280 * Whether the stack of bubbles is expanded or not.
281 */
282 public boolean isExpanded() {
283 return mIsExpanded;
284 }
285
286 /**
287 * The {@link BubbleView} that is expanded, null if one does not exist.
288 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800289 BubbleView getExpandedBubbleView() {
290 return mExpandedBubble != null ? mExpandedBubble.iconView : null;
291 }
292
293 /**
294 * The {@link Bubble} that is expanded, null if one does not exist.
295 */
296 Bubble getExpandedBubble() {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800297 return mExpandedBubble;
298 }
299
300 /**
301 * Sets the bubble that should be expanded and expands if needed.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800302 *
303 * @param key the {@link NotificationEntry#key} associated with the bubble to expand.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800304 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800305 void setExpandedBubble(String key) {
306 Bubble bubbleToExpand = mBubbleData.getBubble(key);
Mady Melloracb12152019-01-29 15:24:48 -0800307 if (mIsExpanded && !bubbleToExpand.equals(mExpandedBubble)) {
308 // Previously expanded, notify that this bubble is no longer expanded
Mady Mellor3dff9e62019-02-05 18:12:53 -0800309 notifyExpansionChanged(mExpandedBubble.entry, false /* expanded */);
Mady Melloracb12152019-01-29 15:24:48 -0800310 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800311 Bubble prevBubble = mExpandedBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800312 mExpandedBubble = bubbleToExpand;
Mady Mellorde2d4d22019-01-29 14:15:34 -0800313 if (!mIsExpanded) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800314 // If we weren't previously expanded we should animate open.
315 animateExpansion(true /* expand */);
Steven Wua254dab2019-01-29 11:30:39 -0500316 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800317 } else {
Mady Mellorde2d4d22019-01-29 14:15:34 -0800318 // Otherwise just update the views
319 // TODO: probably animate / page to expanded one
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800320 updateExpandedBubble();
Mady Mellorde2d4d22019-01-29 14:15:34 -0800321 updatePointerPosition();
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800322 requestUpdate();
Steven Wua254dab2019-01-29 11:30:39 -0500323 logBubbleEvent(prevBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
324 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800325 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800326 mExpandedBubble.entry.setShowInShadeWhenBubble(false);
327 notifyExpansionChanged(mExpandedBubble.entry, true /* expanded */);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800328 }
329
330 /**
Mady Melloredd4ee12019-01-18 10:45:11 -0800331 * Sets the entry that should be expanded and expands if needed.
332 */
333 @VisibleForTesting
334 public void setExpandedBubble(NotificationEntry entry) {
335 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
336 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
337 if (entry.equals(bv.getEntry())) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800338 setExpandedBubble(entry.key);
Mady Melloredd4ee12019-01-18 10:45:11 -0800339 }
340 }
341 }
342
343 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800344 * Adds a bubble to the top of the stack.
345 *
Mady Mellor3dff9e62019-02-05 18:12:53 -0800346 * @param entry the notification to add to the stack of bubbles.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800347 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800348 public void addBubble(NotificationEntry entry) {
349 Bubble b = new Bubble(entry, mInflater, this /* stackView */, mBlockedListener);
350 mBubbleData.addBubble(b);
351
352 mBubbleContainer.addView(b.iconView, 0,
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800353 new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
Mady Mellor3dff9e62019-02-05 18:12:53 -0800354 ViewClippingUtil.setClippingDeactivated(b.iconView, true, mClippingParameters);
355
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800356 requestUpdate();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800357 logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800358 }
359
360 /**
361 * Remove a bubble from the stack.
362 */
Mark Renouf08bc42a2019-03-07 13:01:59 -0500363 public void removeBubble(String key, int reason) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800364 Bubble b = mBubbleData.removeBubble(key);
365 if (b == null) {
366 return;
367 }
Mark Renouf08bc42a2019-03-07 13:01:59 -0500368 int removedIndex = dismissBubble(b, reason);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800369 int bubbleCount = mBubbleContainer.getChildCount();
Mark Renouf658c6bc2019-01-30 10:26:54 -0500370 if (bubbleCount == 0) {
371 // If no bubbles remain, collapse the entire stack.
372 collapseStack();
373 return;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800374 } else if (b.equals(mExpandedBubble)) {
Mark Renouf658c6bc2019-01-30 10:26:54 -0500375 // Was the current bubble just removed?
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800376 // If we have other bubbles and are expanded go to the next one or previous
377 // if the bubble removed was last
378 int nextIndex = bubbleCount > removedIndex ? removedIndex : bubbleCount - 1;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800379 BubbleView expandedBubble = (BubbleView) mBubbleContainer.getChildAt(nextIndex);
Mark Renouf658c6bc2019-01-30 10:26:54 -0500380 if (mIsExpanded) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800381 setExpandedBubble(expandedBubble.getKey());
Mark Renouf658c6bc2019-01-30 10:26:54 -0500382 } else {
383 mExpandedBubble = null;
384 }
Mady Mellorcd9b1302018-11-06 18:08:04 -0800385 }
Mark Renouf08bc42a2019-03-07 13:01:59 -0500386 // TODO: consider logging reason code
Mady Mellor3dff9e62019-02-05 18:12:53 -0800387 logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800388 }
389
390 /**
391 * Dismiss the stack of bubbles.
392 */
Mark Renouf08bc42a2019-03-07 13:01:59 -0500393 public void stackDismissed(int reason) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800394 for (Bubble bubble : mBubbleData.getBubbles()) {
Mark Renouf08bc42a2019-03-07 13:01:59 -0500395 dismissBubble(bubble, reason);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800396 }
397 mBubbleData.clear();
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800398 collapseStack();
399 mBubbleContainer.removeAllViews();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800400 mExpandedViewContainer.removeAllViews();
Mark Renouf08bc42a2019-03-07 13:01:59 -0500401 // TODO: consider logging reason code
Steven Wua254dab2019-01-29 11:30:39 -0500402 logBubbleEvent(null /* no bubble associated with bubble stack dismiss */,
403 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_DISMISSED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800404 }
405
406 /**
Mark Renouf08bc42a2019-03-07 13:01:59 -0500407 * Marks the notification entry as dismissed, cleans up Bubble icon and expanded view UI
408 * elements and calls deleteIntent if necessary.
409 *
410 * <p>Note: This does not remove the Bubble from BubbleData.
411 *
412 * @param bubble the Bubble being dismissed
413 * @param reason code for the reason the dismiss was triggered
414 * @see BubbleController.DismissReason
415 */
416 private int dismissBubble(Bubble bubble, @DismissReason int reason) {
417 if (DEBUG) {
418 Log.d(TAG, "dismissBubble: " + bubble + " reason=" + reason);
419 }
420 bubble.entry.setBubbleDismissed(true);
421 bubble.expandedView.cleanUpExpandedState();
422
423 // Remove it from the views
424 int removedIndex = mBubbleContainer.indexOfChild(bubble.iconView);
425 mBubbleContainer.removeViewAt(removedIndex);
426
427 if (reason == BubbleController.DISMISS_USER_GESTURE) {
428 Notification.BubbleMetadata bubbleMetadata = bubble.entry.getBubbleMetadata();
429 PendingIntent deleteIntent = bubbleMetadata.getDeleteIntent();
430 if (deleteIntent != null) {
431 try {
432 deleteIntent.send();
433 } catch (PendingIntent.CanceledException e) {
434 Log.w(TAG, "Failed to send delete intent for bubble with key: "
435 + (bubble.entry != null ? bubble.entry.key : " null entry"));
436 }
437 }
438 }
439 return removedIndex;
440 }
441
442 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800443 * Updates a bubble in the stack.
444 *
Mady Mellor3dff9e62019-02-05 18:12:53 -0800445 * @param entry the entry to update in the stack.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800446 * @param updatePosition whether this bubble should be moved to top of the stack.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800447 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800448 public void updateBubble(NotificationEntry entry, boolean updatePosition) {
449 Bubble b = mBubbleData.getBubble(entry.key);
Mady Mellorfe7ec032019-01-30 17:32:49 -0800450 mBubbleData.updateBubble(entry.key, entry);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800451
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800452 if (updatePosition && !mIsExpanded) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800453 // If alerting it gets promoted to top of the stack.
Mady Mellor3dff9e62019-02-05 18:12:53 -0800454 if (mBubbleContainer.indexOfChild(b.iconView) != 0) {
455 mBubbleContainer.moveViewTo(b.iconView, 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800456 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800457 requestUpdate();
458 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800459 if (mIsExpanded && entry.equals(mExpandedBubble.entry)) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800460 entry.setShowInShadeWhenBubble(false);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800461 requestUpdate();
462 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800463 logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800464 }
465
466 /**
467 * @return the view the touch event is on
468 */
469 @Nullable
470 public View getTargetView(MotionEvent event) {
471 float x = event.getRawX();
472 float y = event.getRawY();
473 if (mIsExpanded) {
474 if (isIntersecting(mBubbleContainer, x, y)) {
475 for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
476 BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
477 if (isIntersecting(view, x, y)) {
478 return view;
479 }
480 }
481 } else if (isIntersecting(mExpandedViewContainer, x, y)) {
482 return mExpandedViewContainer;
483 }
484 // Outside parts of view we care about.
485 return null;
486 }
487 // If we're collapsed, the stack is always the target.
488 return this;
489 }
490
491 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800492 * Collapses the stack of bubbles.
Mark Renoufcecc77b2019-01-30 16:32:24 -0500493 * <p>
494 * Must be called from the main thread.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800495 */
Mark Renoufcecc77b2019-01-30 16:32:24 -0500496 @MainThread
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800497 public void collapseStack() {
498 if (mIsExpanded) {
499 // TODO: Save opened bubble & move it to top of stack
500 animateExpansion(false /* shouldExpand */);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800501 notifyExpansionChanged(mExpandedBubble.entry, mIsExpanded);
Steven Wua254dab2019-01-29 11:30:39 -0500502 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800503 }
504 }
505
Mady Mellor9801e852019-01-22 14:50:28 -0800506 void collapseStack(Runnable endRunnable) {
507 collapseStack();
508 // TODO - use the runnable at end of animation
509 endRunnable.run();
510 }
511
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800512 /**
Mady Mellor94d94a72019-03-05 18:16:59 -0800513 * Expands the stack of bubbles.
Mark Renoufcecc77b2019-01-30 16:32:24 -0500514 * <p>
515 * Must be called from the main thread.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800516 */
Mark Renoufcecc77b2019-01-30 16:32:24 -0500517 @MainThread
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800518 public void expandStack() {
519 if (!mIsExpanded) {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800520 String expandedBubbleKey = getBubbleAt(0).getKey();
521 setExpandedBubble(expandedBubbleKey);
Steven Wua254dab2019-01-29 11:30:39 -0500522 logBubbleEvent(mExpandedBubble, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800523 }
524 }
525
526 /**
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800527 * Tell the stack to animate to collapsed or expanded state.
528 */
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800529 private void animateExpansion(boolean shouldExpand) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800530 if (mIsExpanded != shouldExpand) {
531 mIsExpanded = shouldExpand;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800532 updateExpandedBubble();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800533 applyCurrentState();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800534
535 mIsAnimating = true;
536
537 Runnable updateAfter = () -> {
538 applyCurrentState();
539 mIsAnimating = false;
540 requestUpdate();
541 };
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800542
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800543 if (shouldExpand) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800544 mBubbleContainer.setController(mExpandedAnimationController);
545 mExpandedAnimationController.expandFromStack(
Joshua Tsuji3829caa2019-03-05 18:09:13 -0500546 /* collapseTo */
547 mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
548 /* after */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500549 () -> {
550 updatePointerPosition();
551 updateAfter.run();
Mady Mellorde2d4d22019-01-29 14:15:34 -0800552 });
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800553 } else {
554 mBubbleContainer.cancelAllAnimations();
555 mExpandedAnimationController.collapseBackToStack(
556 () -> {
557 mBubbleContainer.setController(mStackAnimationController);
558 updateAfter.run();
559 });
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800560 }
561
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800562 final float xStart =
563 mStackAnimationController.getStackPosition().x < getWidth() / 2
564 ? -mExpandedAnimateXDistance
565 : mExpandedAnimateXDistance;
566
567 final float yStart = Math.min(
568 mStackAnimationController.getStackPosition().y,
569 mExpandedAnimateYDistance);
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800570 final float yDest = getYPositionForExpandedView();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800571
572 if (shouldExpand) {
573 mExpandedViewContainer.setTranslationX(xStart);
574 mExpandedViewContainer.setTranslationY(yStart);
575 mExpandedViewContainer.setAlpha(0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800576 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800577
578 mExpandedViewXAnim.animateToFinalPosition(shouldExpand ? 0f : xStart);
579 mExpandedViewYAnim.animateToFinalPosition(shouldExpand ? yDest : yStart);
580 mExpandedViewContainer.animate()
581 .setDuration(100)
582 .alpha(shouldExpand ? 1f : 0f);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800583 }
584 }
585
586 /**
587 * The width of the collapsed stack of bubbles.
588 */
589 public int getStackWidth() {
590 return mBubblePadding * (mBubbleContainer.getChildCount() - 1)
591 + mBubbleSize + mBubbleContainer.getPaddingEnd()
592 + mBubbleContainer.getPaddingStart();
593 }
594
Mady Mellor3dff9e62019-02-05 18:12:53 -0800595 private void notifyExpansionChanged(NotificationEntry entry, boolean expanded) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800596 if (mExpandListener != null) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800597 mExpandListener.onBubbleExpandChanged(expanded, entry != null ? entry.key : null);
598 }
599 }
600
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800601 /** Return the BubbleView at the given index from the bubble container. */
602 public BubbleView getBubbleAt(int i) {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800603 return mBubbleContainer.getChildCount() > i
604 ? (BubbleView) mBubbleContainer.getChildAt(i)
605 : null;
606 }
607
Joshua Tsujia19515f2019-02-13 18:02:29 -0500608 /** Moves the bubbles out of the way if they're going to be over the keyboard. */
609 public void onImeVisibilityChanged(boolean visible, int height) {
610 if (!mIsExpanded) {
611 if (visible) {
612 mStackAnimationController.updateBoundsForVisibleImeAndAnimate(height + mImeOffset);
613 } else {
614 mStackAnimationController.updateBoundsForInvisibleImeAndAnimate();
615 }
616 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800617 }
618
619 /** Called when a drag operation on an individual bubble has started. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500620 public void onBubbleDragStart(View bubble) {
621 mExpandedAnimationController.prepareForBubbleDrag(bubble);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800622 }
623
624 /** Called with the coordinates to which an individual bubble has been dragged. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500625 public void onBubbleDragged(View bubble, float x, float y) {
626 if (!mIsExpanded || mIsAnimating) {
627 return;
628 }
629
630 mExpandedAnimationController.dragBubbleOut(bubble, x, y);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800631 }
632
633 /** Called when a drag operation on an individual bubble has finished. */
Joshua Tsuji442b6272019-02-08 13:23:43 -0500634 public void onBubbleDragFinish(
635 View bubble, float x, float y, float velX, float velY, boolean dismissed) {
636 if (!mIsExpanded || mIsAnimating) {
637 return;
638 }
639
640 if (dismissed) {
641 mExpandedAnimationController.prepareForDismissalWithVelocity(bubble, velX, velY);
642 } else {
643 mExpandedAnimationController.snapBubbleBack(bubble, velX, velY);
644 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800645 }
646
647 void onDragStart() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500648 if (mIsExpanded || mIsAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800649 return;
650 }
651
652 mStackAnimationController.cancelStackPositionAnimations();
653 mBubbleContainer.setController(mStackAnimationController);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800654 }
655
656 void onDragged(float x, float y) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500657 if (mIsExpanded || mIsAnimating) {
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800658 return;
659 }
660
661 mStackAnimationController.moveFirstBubbleWithStackFollowing(x, y);
662 }
663
664 void onDragFinish(float x, float y, float velX, float velY) {
665 // TODO: Add fling to bottom to dismiss.
666
667 if (mIsExpanded || mIsAnimating) {
668 return;
669 }
670
Joshua Tsujie13b4fc2019-02-28 18:39:57 -0500671 mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY);
Steven Wua254dab2019-01-29 11:30:39 -0500672 logBubbleEvent(null /* no bubble associated with bubble stack move */,
673 StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800674 }
675
676 /**
Mady Mellorfe7ec032019-01-30 17:32:49 -0800677 * Calculates how large the expanded view of the bubble can be. This takes into account the
678 * y position when the bubbles are expanded as well as the bounds of the dismiss target.
679 */
680 int getMaxExpandedHeight() {
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800681 boolean showOnTop = BubbleController.showBubblesAtTop(getContext());
Mady Mellorfe7ec032019-01-30 17:32:49 -0800682 int expandedY = (int) mExpandedAnimationController.getExpandedY();
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800683 if (showOnTop) {
684 // PIP dismiss view uses FLAG_LAYOUT_IN_SCREEN so we need to subtract the bottom inset
685 int pipDismissHeight = mPipDismissHeight - getBottomInset();
686 return mDisplaySize.y - expandedY - mBubbleSize - pipDismissHeight;
687 } else {
688 return expandedY - getStatusBarHeight();
689 }
690 }
691
692 /**
693 * Calculates the y position of the expanded view when it is expanded.
694 */
695 float getYPositionForExpandedView() {
696 boolean showOnTop = BubbleController.showBubblesAtTop(getContext());
697 if (showOnTop) {
698 return getStatusBarHeight() + mBubbleSize + mBubblePadding;
699 } else {
700 return mExpandedAnimationController.getExpandedY()
701 - mExpandedBubble.expandedView.getExpandedSize() - mBubblePadding;
702 }
703 }
704
705 /**
706 * Called when the height of the currently expanded view has changed (not via an
707 * update to the bubble's desired height but for some other reason, e.g. permission view
708 * goes away).
709 */
710 void onExpandedHeightChanged() {
711 if (mIsExpanded) {
712 requestUpdate();
713 }
Mady Mellorfe7ec032019-01-30 17:32:49 -0800714 }
715
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800716 @Override
717 public void getBoundsOnScreen(Rect outRect) {
718 if (!mIsExpanded) {
Mady Mellor217b2e92019-02-27 11:44:16 -0800719 if (mBubbleContainer.getChildCount() > 0) {
720 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
721 }
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800722 } else {
723 mBubbleContainer.getBoundsOnScreen(outRect);
724 }
725 }
726
727 private int getStatusBarHeight() {
728 if (getRootWindowInsets() != null) {
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500729 WindowInsets insets = getRootWindowInsets();
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800730 return Math.max(
Joshua Tsujif44347f2019-02-12 14:28:06 -0500731 mStatusBarHeight,
Joshua Tsuji0fee7682019-01-25 11:37:49 -0500732 insets.getDisplayCutout() != null
733 ? insets.getDisplayCutout().getSafeInsetTop()
734 : 0);
Joshua Tsujib1a796b2019-01-16 15:43:12 -0800735 }
736
737 return 0;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800738 }
739
Mady Mellorfe7ec032019-01-30 17:32:49 -0800740 private int getBottomInset() {
741 if (getRootWindowInsets() != null) {
742 WindowInsets insets = getRootWindowInsets();
743 return insets.getSystemWindowInsetBottom();
744 }
745 return 0;
746 }
747
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800748 private boolean isIntersecting(View view, float x, float y) {
749 mTempLoc = view.getLocationOnScreen();
750 mTempRect.set(mTempLoc[0], mTempLoc[1], mTempLoc[0] + view.getWidth(),
751 mTempLoc[1] + view.getHeight());
752 return mTempRect.contains(x, y);
753 }
754
755 private void requestUpdate() {
756 if (mViewUpdatedRequested || mIsAnimating) {
757 return;
758 }
759 mViewUpdatedRequested = true;
760 getViewTreeObserver().addOnPreDrawListener(mViewUpdater);
761 invalidate();
762 }
763
764 private void updateExpandedBubble() {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800765 mExpandedViewContainer.removeAllViews();
766 if (mExpandedBubble != null && mIsExpanded) {
767 mExpandedViewContainer.addView(mExpandedBubble.expandedView);
Mady Mellor5029fa62019-03-05 12:16:21 -0800768 mExpandedBubble.expandedView.populateExpandedView();
Mady Mellor3dff9e62019-02-05 18:12:53 -0800769 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800770 }
771 }
772
773 private void applyCurrentState() {
Mark Renouf89b1a4a2018-12-04 14:59:45 -0500774 Log.d(TAG, "applyCurrentState: mIsExpanded=" + mIsExpanded);
775
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800776 mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800777 if (mIsExpanded) {
Mady Mellor5029fa62019-03-05 12:16:21 -0800778 // First update the view so that it calculates a new height (ensuring the y position
779 // calculation is correct)
780 mExpandedBubble.expandedView.updateView();
Mady Mellor44ee2fe2019-01-30 17:51:16 -0800781 final float y = getYPositionForExpandedView();
782 mExpandedViewContainer.setTranslationY(y);
Mady Mellor5029fa62019-03-05 12:16:21 -0800783 // Then update the view so that ActivityView knows we translated
Mady Mellor3dff9e62019-02-05 18:12:53 -0800784 mExpandedBubble.expandedView.updateView();
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800785 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800786
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800787 int bubbsCount = mBubbleContainer.getChildCount();
788 for (int i = 0; i < bubbsCount; i++) {
789 BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800790 bv.updateDotVisibility();
791 bv.setZ(bubbsCount - i);
Joshua Tsuji580c0bf2019-01-28 13:28:21 -0500792
793 // Draw the shadow around the circle inscribed within the bubble's bounds. This
794 // (intentionally) does not draw a shadow behind the update dot, which should be drawing
795 // its own shadow since it's on a different (higher) plane.
796 bv.setOutlineProvider(new ViewOutlineProvider() {
797 @Override
798 public void getOutline(View view, Outline outline) {
799 outline.setOval(0, 0, mBubbleSize, mBubbleSize);
800 }
801 });
802 bv.setClipToOutline(false);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800803 }
804 }
805
Mady Mellorde2d4d22019-01-29 14:15:34 -0800806 private void updatePointerPosition() {
807 if (mExpandedBubble != null) {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500808 float pointerPosition = mExpandedBubble.iconView.getTranslationX()
Mady Mellor3dff9e62019-02-05 18:12:53 -0800809 + (mExpandedBubble.iconView.getWidth() / 2f);
Joshua Tsuji442b6272019-02-08 13:23:43 -0500810 mExpandedBubble.expandedView.setPointerPosition((int) pointerPosition);
Mady Mellorde2d4d22019-01-29 14:15:34 -0800811 }
812 }
813
Steven Wua254dab2019-01-29 11:30:39 -0500814 /**
815 * @return the number of bubbles in the stack view.
816 */
Steven Wub00225b2019-02-08 14:27:42 -0500817 public int getBubbleCount() {
Steven Wua254dab2019-01-29 11:30:39 -0500818 return mBubbleContainer.getChildCount();
819 }
820
821 /**
822 * Finds the bubble index within the stack.
823 *
Mady Mellor3dff9e62019-02-05 18:12:53 -0800824 * @param bubble the bubble to look up.
Steven Wua254dab2019-01-29 11:30:39 -0500825 * @return the index of the bubble view within the bubble stack. The range of the position
826 * is between 0 and the bubble count minus 1.
827 */
Steven Wua62cb6a2019-02-15 17:12:51 -0500828 int getBubbleIndex(@Nullable Bubble bubble) {
829 if (bubble == null) {
830 return 0;
831 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800832 return mBubbleContainer.indexOfChild(bubble.iconView);
Steven Wua254dab2019-01-29 11:30:39 -0500833 }
834
835 /**
836 * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
837 */
Steven Wub00225b2019-02-08 14:27:42 -0500838 public float getNormalizedXPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500839 return new BigDecimal(getStackPosition().x / mDisplaySize.x)
Steven Wua254dab2019-01-29 11:30:39 -0500840 .setScale(4, RoundingMode.CEILING.HALF_UP)
841 .floatValue();
842 }
843
844 /**
845 * @return the normalized y-axis position of the bubble stack rounded to 4 decimal places.
846 */
Steven Wub00225b2019-02-08 14:27:42 -0500847 public float getNormalizedYPosition() {
Joshua Tsuji442b6272019-02-08 13:23:43 -0500848 return new BigDecimal(getStackPosition().y / mDisplaySize.y)
Steven Wua254dab2019-01-29 11:30:39 -0500849 .setScale(4, RoundingMode.CEILING.HALF_UP)
850 .floatValue();
851 }
852
Joshua Tsujia19515f2019-02-13 18:02:29 -0500853 public PointF getStackPosition() {
854 return mStackAnimationController.getStackPosition();
855 }
856
Steven Wua254dab2019-01-29 11:30:39 -0500857 /**
858 * Logs the bubble UI event.
859 *
860 * @param bubble the bubble that is being interacted on. Null value indicates that
861 * the user interaction is not specific to one bubble.
862 * @param action the user interaction enum.
863 */
Mady Mellor3dff9e62019-02-05 18:12:53 -0800864 private void logBubbleEvent(@Nullable Bubble bubble, int action) {
Steven Wua254dab2019-01-29 11:30:39 -0500865 if (bubble == null) {
866 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
867 null /* package name */,
868 null /* notification channel */,
869 0 /* notification ID */,
870 0 /* bubble position */,
871 getBubbleCount(),
872 action,
873 getNormalizedXPosition(),
874 getNormalizedYPosition());
875 } else {
Mady Mellor3dff9e62019-02-05 18:12:53 -0800876 StatusBarNotification notification = bubble.entry.notification;
Steven Wua254dab2019-01-29 11:30:39 -0500877 StatsLog.write(StatsLog.BUBBLE_UI_CHANGED,
878 notification.getPackageName(),
879 notification.getNotification().getChannelId(),
880 notification.getId(),
881 getBubbleIndex(bubble),
882 getBubbleCount(),
883 action,
884 getNormalizedXPosition(),
885 getNormalizedYPosition());
886 }
887 }
Mark Renouf041d7262019-02-06 12:09:41 -0500888
889 /**
890 * Called when a back gesture should be directed to the Bubbles stack. When expanded,
891 * a back key down/up event pair is forwarded to the bubble Activity.
892 */
893 boolean performBackPressIfNeeded() {
894 if (!isExpanded()) {
895 return false;
896 }
897 return mExpandedBubble.expandedView.performBackPressIfNeeded();
898 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800899}