• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 
17 package com.android.wm.shell.bubbles;
18 
19 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
20 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
21 
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.graphics.Insets;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.graphics.RectF;
28 import android.view.Surface;
29 import android.view.WindowManager;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.internal.protolog.ProtoLog;
35 import com.android.wm.shell.Flags;
36 import com.android.wm.shell.R;
37 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
38 import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
39 import com.android.wm.shell.shared.bubbles.DeviceConfig;
40 
41 /**
42  * Keeps track of display size, configuration, and specific bubble sizes. One place for all
43  * placement and positioning calculations to refer to.
44  */
45 public class BubblePositioner implements BubbleDropTargetBoundsProvider {
46 
47     /** The screen edge the bubble stack is pinned to */
48     public enum StackPinnedEdge {
49         LEFT,
50         RIGHT
51     }
52 
53     /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
54     public static final int NUM_VISIBLE_WHEN_RESTING = 2;
55     /** Indicates a bubble's height should be the maximum available space. **/
56     public static final int MAX_HEIGHT = -1;
57     /** The max percent of screen width to use for the flyout on large screens. */
58     public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f;
59     /** The max percent of screen width to use for the flyout on phone. */
60     public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f;
61     /** The percent of screen width for the expanded view on a small tablet. **/
62     private static final float EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT = 0.72f;
63     /** The percent of screen width for the expanded view when shown in the bubble bar. **/
64     private static final float EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT = 0.7f;
65     /** The percent of screen width for the expanded view when shown in the bubble bar. **/
66     private static final float EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT = 0.4f;
67 
68     private Context mContext;
69     private DeviceConfig mDeviceConfig;
70     private Rect mScreenRect;
71     private @Surface.Rotation int mRotation = Surface.ROTATION_0;
72     private Insets mInsets;
73     private boolean mImeVisible;
74     /**
75      * The height of the IME excluding the bottom inset. If the IME is 100 pixels tall and we have
76      * 20 pixels bottom inset, the IME height is adjusted to 80 to represent the overlap with the
77      * Bubbles window.
78      */
79     private int mImeHeight;
80     private Rect mPositionRect;
81     private int mDefaultMaxBubbles;
82     private int mMaxBubbles;
83     private int mBubbleSize;
84     private int mSpacingBetweenBubbles;
85     private int mBubblePaddingTop;
86     private int mBubbleOffscreenAmount;
87     private int mStackOffset;
88     private int mBubbleElevation;
89 
90     private int mExpandedViewMinHeight;
91     private int mExpandedViewLargeScreenWidth;
92     private int mExpandedViewLargeScreenInsetClosestEdge;
93     private int mExpandedViewLargeScreenInsetFurthestEdge;
94     private int mExpandedViewBubbleBarWidth;
95 
96     private int mOverflowWidth;
97     private int mExpandedViewPadding;
98     private int mPointerMargin;
99     private int mPointerWidth;
100     private int mPointerHeight;
101     private int mPointerOverlap;
102     private int mManageButtonHeightIncludingMargins;
103     private int mManageButtonHeight;
104     private int mOverflowHeight;
105     private int mMinimumFlyoutWidthLargeScreen;
106     private int mBarExpViewDropTargetPaddingTop;
107     private int mBarExpViewDropTargetPaddingBottom;
108     private int mBarExpViewDropTargetPaddingHorizontal;
109     private int mBarDropTargetWidth;
110     private int mBarDropTargetHeight;
111 
112     private PointF mRestingStackPosition;
113 
114     private boolean mShowingInBubbleBar;
115     private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT;
116     private int mBubbleBarTopOnScreen;
117 
BubblePositioner(Context context, WindowManager windowManager)118     public BubblePositioner(Context context, WindowManager windowManager) {
119         this(context, DeviceConfig.create(context, windowManager));
120     }
121 
BubblePositioner(Context context, DeviceConfig deviceConfig)122     public BubblePositioner(Context context, DeviceConfig deviceConfig) {
123         mContext = context;
124         mDeviceConfig = deviceConfig;
125         update(deviceConfig);
126     }
127 
128     /**
129      * Available space and inset information. Call this when config changes
130      * occur or when added to a window.
131      */
update(DeviceConfig deviceConfig)132     public void update(DeviceConfig deviceConfig) {
133         mDeviceConfig = deviceConfig;
134         ProtoLog.d(WM_SHELL_BUBBLES, "update positioner: "
135                         + "rotation=%d insets=%s largeScreen=%b "
136                         + "smallTablet=%b isBubbleBar=%b bounds=%s",
137                 mRotation, deviceConfig.getInsets(), deviceConfig.isLargeScreen(),
138                 deviceConfig.isSmallTablet(), mShowingInBubbleBar,
139                 deviceConfig.getWindowBounds());
140         updateInternal(mRotation, deviceConfig.getInsets(), deviceConfig.getWindowBounds());
141     }
142 
143     /** Returns the device config being used. */
getCurrentConfig()144     public DeviceConfig getCurrentConfig() {
145         return mDeviceConfig;
146     }
147 
148     @VisibleForTesting
updateInternal(int rotation, Insets insets, Rect bounds)149     public void updateInternal(int rotation, Insets insets, Rect bounds) {
150         BubbleStackView.RelativeStackPosition prevStackPosition = null;
151         if (mRestingStackPosition != null && mScreenRect != null && !mScreenRect.equals(bounds)) {
152             // Save the resting position as a relative position with the previous bounds, at the
153             // end of the update we'll restore it based on the new bounds.
154             prevStackPosition = new BubbleStackView.RelativeStackPosition(getRestingPosition(),
155                     getAllowableStackPositionRegion(1));
156         }
157         mRotation = rotation;
158         mInsets = insets;
159 
160         mScreenRect = new Rect(bounds);
161         mPositionRect = new Rect(bounds);
162         mPositionRect.left += mInsets.left;
163         mPositionRect.top += mInsets.top;
164         mPositionRect.right -= mInsets.right;
165         mPositionRect.bottom -= mInsets.bottom;
166 
167         Resources res = mContext.getResources();
168         mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
169         mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
170         mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
171         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
172         mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
173         mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
174         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
175         mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
176         mExpandedViewBubbleBarWidth = Math.min(
177                 res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width),
178                 mPositionRect.width() - 2 * mExpandedViewPadding
179         );
180         mBarExpViewDropTargetPaddingTop = res.getDimensionPixelSize(
181                 R.dimen.bubble_bar_expanded_view_drop_target_padding_top);
182         mBarExpViewDropTargetPaddingBottom = res.getDimensionPixelSize(
183                 R.dimen.bubble_bar_expanded_view_drop_target_padding_bottom);
184         mBarExpViewDropTargetPaddingHorizontal = res.getDimensionPixelSize(
185                 R.dimen.bubble_bar_expanded_view_drop_target_padding_horizontal);
186         mBarDropTargetWidth = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_width);
187         mBarDropTargetHeight = res.getDimensionPixelSize(R.dimen.bubble_bar_drop_target_height);
188 
189         if (mShowingInBubbleBar) {
190             mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth;
191         } else if (mDeviceConfig.isSmallTablet()) {
192             mExpandedViewLargeScreenWidth = (int) (bounds.width()
193                     * EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
194         } else {
195             int expandedViewLargeScreenSpacing = res.getDimensionPixelSize(
196                     R.dimen.bubble_expanded_view_largescreen_landscape_padding);
197             mExpandedViewLargeScreenWidth = Math.min(
198                     res.getDimensionPixelSize(R.dimen.bubble_expanded_view_largescreen_width),
199                     bounds.width() - expandedViewLargeScreenSpacing * 2);
200         }
201         if (mDeviceConfig.isLargeScreen()) {
202             if (mDeviceConfig.isSmallTablet()) {
203                 final int centeredInset = (bounds.width() - mExpandedViewLargeScreenWidth) / 2;
204                 mExpandedViewLargeScreenInsetClosestEdge = centeredInset;
205                 mExpandedViewLargeScreenInsetFurthestEdge = centeredInset;
206             } else {
207                 mExpandedViewLargeScreenInsetClosestEdge = res.getDimensionPixelSize(
208                         R.dimen.bubble_expanded_view_largescreen_landscape_padding);
209                 mExpandedViewLargeScreenInsetFurthestEdge = bounds.width()
210                         - mExpandedViewLargeScreenInsetClosestEdge
211                         - mExpandedViewLargeScreenWidth;
212             }
213         } else {
214             mExpandedViewLargeScreenInsetClosestEdge = mExpandedViewPadding;
215             mExpandedViewLargeScreenInsetFurthestEdge = mExpandedViewPadding;
216         }
217 
218         mOverflowWidth = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_overflow_width);
219         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
220         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
221         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
222         mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
223         mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height);
224         mManageButtonHeightIncludingMargins =
225                 mManageButtonHeight
226                 + 2 * res.getDimensionPixelSize(R.dimen.bubble_manage_button_margin);
227         mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
228         mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
229         mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize(
230                 R.dimen.bubbles_flyout_min_width_large_screen);
231 
232         mMaxBubbles = calculateMaxBubbles();
233 
234         if (prevStackPosition != null) {
235             // Get the new resting position based on the updated values
236             mRestingStackPosition = prevStackPosition.getAbsolutePositionInRegion(
237                     getAllowableStackPositionRegion(1));
238         }
239     }
240 
241     /**
242      * @return the maximum number of bubbles that can fit on the screen when expanded. If the
243      * screen size / screen density is too small to support the default maximum number, then
244      * the number will be adjust to something lower to ensure everything is presented nicely.
245      */
calculateMaxBubbles()246     private int calculateMaxBubbles() {
247         // Use the shortest edge.
248         // In portrait the bubbles should align with the expanded view so subtract its padding.
249         // We always show the overflow so subtract one bubble size.
250         int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2);
251         int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height())
252                 - padding
253                 - mBubbleSize;
254         // Each of the bubbles have spacing because the overflow is at the end.
255         int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles);
256         if (howManyFit < mDefaultMaxBubbles) {
257             // Not enough space for the default.
258             return howManyFit;
259         }
260         return mDefaultMaxBubbles;
261     }
262 
263 
264     /**
265      * @return a rect of available screen space accounting for orientation, system bars and cutouts.
266      * Does not account for IME.
267      */
getAvailableRect()268     public Rect getAvailableRect() {
269         return mPositionRect;
270     }
271 
272     /**
273      * @return a rect of the screen size.
274      */
getScreenRect()275     public Rect getScreenRect() {
276         return mScreenRect;
277     }
278 
279     /**
280      * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
281      * inset is not included here.
282      */
getInsets()283     public Insets getInsets() {
284         return mInsets;
285     }
286 
287     /** @return whether the device is in landscape orientation. */
isLandscape()288     public boolean isLandscape() {
289         return mDeviceConfig.isLandscape();
290     }
291 
292     /**
293      * On large screen (not small tablet), while in portrait, expanded bubbles are aligned to
294      * the bottom of the screen.
295      *
296      * @return whether bubbles are bottom aligned while expanded
297      */
areBubblesBottomAligned()298     public boolean areBubblesBottomAligned() {
299         return isLargeScreen()
300                 && !mDeviceConfig.isSmallTablet()
301                 && !isLandscape();
302     }
303 
304     /** @return whether the screen is considered large. */
isLargeScreen()305     public boolean isLargeScreen() {
306         return mDeviceConfig.isLargeScreen();
307     }
308 
309     /**
310      * Indicates how bubbles appear when expanded.
311      *
312      * When false, bubbles display at the top of the screen with the expanded view
313      * below them. When true, bubbles display at the edges of the screen with the expanded view
314      * to the left or right side.
315      */
showBubblesVertically()316     public boolean showBubblesVertically() {
317         return isLandscape() || mDeviceConfig.isLargeScreen();
318     }
319 
320     /** Size of the bubble. */
getBubbleSize()321     public int getBubbleSize() {
322         return mBubbleSize;
323     }
324 
325     /** The amount of padding at the top of the screen that the bubbles avoid when being placed. */
getBubblePaddingTop()326     public int getBubblePaddingTop() {
327         return mBubblePaddingTop;
328     }
329 
330     /** The amount the stack hang off of the screen when collapsed. */
getStackOffScreenAmount()331     public int getStackOffScreenAmount() {
332         return mBubbleOffscreenAmount;
333     }
334 
335     /** Offset of bubbles in the stack (i.e. how much they overlap). */
getStackOffset()336     public int getStackOffset() {
337         return mStackOffset;
338     }
339 
340     /** Size of the visible (non-overlapping) part of the pointer. */
getPointerSize()341     public int getPointerSize() {
342         return mPointerHeight - mPointerOverlap;
343     }
344 
345     /** The maximum number of bubbles that can be displayed comfortably on screen. */
getMaxBubbles()346     public int getMaxBubbles() {
347         return mMaxBubbles;
348     }
349 
350     /** The height for the IME if it's visible. **/
getImeHeight()351     public int getImeHeight() {
352         return mImeVisible ? mImeHeight : 0;
353     }
354 
355     /** Return top position of the IME if it's visible */
getImeTop()356     public int getImeTop() {
357         if (mImeVisible) {
358             return getScreenRect().bottom - getImeHeight() - getInsets().bottom;
359         }
360         return 0;
361     }
362 
363     /** Returns whether the IME is visible. */
isImeVisible()364     public boolean isImeVisible() {
365         return mImeVisible;
366     }
367 
368     /**
369      * Sets whether the IME is visible and its height.
370      *
371      * @param visible whether the IME is visible
372      * @param height the total height of the IME from the bottom of the physical screen
373      **/
setImeVisible(boolean visible, int height)374     public void setImeVisible(boolean visible, int height) {
375         mImeVisible = visible;
376         // adjust the IME to account for the height as seen by the Bubbles window
377         mImeHeight = visible ? Math.max(height - getInsets().bottom, 0) : 0;
378     }
379 
getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow)380     private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) {
381         if (isOverflow && mDeviceConfig.isLargeScreen()) {
382             return mScreenRect.width()
383                     - mExpandedViewLargeScreenInsetClosestEdge
384                     - mOverflowWidth;
385         }
386         return mExpandedViewLargeScreenInsetFurthestEdge;
387     }
388 
389     /**
390      * Calculates the padding for the bubble expanded view.
391      *
392      * Some specifics:
393      * On large screens the width of the expanded view is restricted via this padding.
394      * On phone landscape the bubble overflow expanded view is also restricted via this padding.
395      * On large screens & landscape no top padding is set, the top position is set via translation.
396      * On phone portrait top padding is set as the space between the tip of the pointer and the
397      * bubble.
398      * When the overflow is shown it doesn't have the manage button to pad out the bottom so
399      * padding is added.
400      */
getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow)401     public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) {
402         final int pointerTotalHeight = getPointerSize();
403         final int expandedViewLargeScreenInsetFurthestEdge =
404                 getExpandedViewLargeScreenInsetFurthestEdge(isOverflow);
405         int[] paddings = new int[4];
406         if (mDeviceConfig.isLargeScreen()) {
407             // Note:
408             // If we're in portrait OR if we're a small tablet, then the two insets values will
409             // be equal. If we're landscape and a large tablet, the two values will be different.
410             // [left, top, right, bottom]
411             paddings[0] = onLeft
412                     ? mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight
413                     : expandedViewLargeScreenInsetFurthestEdge;
414             paddings[1] = 0;
415             paddings[2] = onLeft
416                     ? expandedViewLargeScreenInsetFurthestEdge
417                     : mExpandedViewLargeScreenInsetClosestEdge - pointerTotalHeight;
418             // Overflow doesn't show manage button / get padding from it so add padding here
419             paddings[3] = isOverflow ? mExpandedViewPadding : 0;
420             return paddings;
421         } else {
422             int leftPadding = mInsets.left + mExpandedViewPadding;
423             int rightPadding = mInsets.right + mExpandedViewPadding;
424             if (showBubblesVertically()) {
425                 if (!onLeft) {
426                     rightPadding += mBubbleSize - pointerTotalHeight;
427                     leftPadding += isOverflow
428                             ? (mPositionRect.width() - rightPadding - mOverflowWidth)
429                             : 0;
430                 } else {
431                     leftPadding += mBubbleSize - pointerTotalHeight;
432                     rightPadding += isOverflow
433                             ? (mPositionRect.width() - leftPadding - mOverflowWidth)
434                             : 0;
435                 }
436             }
437             // [left, top, right, bottom]
438             paddings[0] = leftPadding;
439             paddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
440             paddings[2] = rightPadding;
441             paddings[3] = 0;
442             return paddings;
443         }
444     }
445 
446     /** Returns the width of the task view content. */
getTaskViewContentWidth(boolean onLeft)447     public int getTaskViewContentWidth(boolean onLeft) {
448         int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false);
449         int pointerOffset = showBubblesVertically() ? getPointerSize() : 0;
450         return mScreenRect.width() - paddings[0] - paddings[2] - pointerOffset;
451     }
452 
453     /** Gets the y position of the expanded view if it was top-aligned. */
getExpandedViewYTopAligned()454     public int getExpandedViewYTopAligned() {
455         final int top = getAvailableRect().top;
456         if (showBubblesVertically()) {
457             return top - mPointerWidth + mExpandedViewPadding;
458         } else {
459             return top + mBubbleSize + mPointerMargin;
460         }
461     }
462 
463     /**
464      * Calculate the maximum height the expanded view can be depending on where it's placed on
465      * the screen and the size of the elements around it (e.g. padding, pointer, manage button).
466      */
getMaxExpandedViewHeight(boolean isOverflow)467     public int getMaxExpandedViewHeight(boolean isOverflow) {
468         if (mDeviceConfig.isLargeScreen() && !mDeviceConfig.isSmallTablet() && !isOverflow) {
469             return getExpandedViewHeightForLargeScreen();
470         }
471         // Subtract top insets because availableRect.height would account for that
472         int expandedContainerY = getExpandedViewYTopAligned() - getInsets().top;
473         int paddingTop = showBubblesVertically()
474                 ? 0
475                 : mPointerHeight;
476         // Subtract pointer size because it's laid out in LinearLayout with the expanded view.
477         int pointerSize = showBubblesVertically()
478                 ? mPointerWidth
479                 : (mPointerHeight + mPointerMargin);
480         int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeightIncludingMargins;
481         return getAvailableRect().height()
482                 - expandedContainerY
483                 - paddingTop
484                 - pointerSize
485                 - bottomPadding;
486     }
487 
488     /**
489      * Returns the height to use for the expanded view when showing on a large screen.
490      */
getExpandedViewHeightForLargeScreen()491     public int getExpandedViewHeightForLargeScreen() {
492         // the expanded view height on large tablets is calculated based on the shortest screen
493         // size and is the same in both portrait and landscape
494         int maxVerticalInset = Math.max(mInsets.top, mInsets.bottom);
495         int shortestScreenSide = Math.min(getScreenRect().height(), getScreenRect().width());
496         // Subtract pointer size because it's laid out in LinearLayout with the expanded view.
497         return shortestScreenSide - maxVerticalInset * 2
498                 - mManageButtonHeight - mPointerWidth - mExpandedViewPadding * 2;
499     }
500 
501     /**
502      * Determines the height for the bubble, ensuring a minimum height. If the height should be as
503      * big as available, returns {@link #MAX_HEIGHT}.
504      */
getExpandedViewHeight(BubbleViewProvider bubble)505     public float getExpandedViewHeight(BubbleViewProvider bubble) {
506         boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
507         if (isOverflow && showBubblesVertically() && !mDeviceConfig.isLargeScreen()) {
508             // overflow in landscape on phone is max
509             return MAX_HEIGHT;
510         }
511         float desiredHeight = isOverflow
512                 ? mOverflowHeight
513                 : ((Bubble) bubble).getDesiredHeight(mContext);
514         desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight);
515         if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) {
516             return MAX_HEIGHT;
517         }
518         return desiredHeight;
519     }
520 
521     /**
522      * Gets the y position for the expanded view. This is the position on screen of the top
523      * horizontal line of the expanded view.
524      *
525      * @param bubble the bubble being positioned.
526      * @param bubblePosition the x position of the bubble if showing on top, the y position of the
527      *                       bubble if showing vertically.
528      * @return the y position for the expanded view.
529      */
getExpandedViewY(BubbleViewProvider bubble, float bubblePosition)530     public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) {
531         boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
532         float expandedViewHeight = getExpandedViewHeight(bubble);
533         int topAlignment = getExpandedViewYTopAligned();
534         int manageButtonHeight =
535                 isOverflow ? mExpandedViewPadding : mManageButtonHeightIncludingMargins;
536 
537         // On large screen portrait bubbles are bottom aligned.
538         if (areBubblesBottomAligned() && expandedViewHeight == MAX_HEIGHT) {
539             return mPositionRect.bottom - manageButtonHeight
540                     - getExpandedViewHeightForLargeScreen() - mPointerWidth;
541         }
542 
543         if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) {
544             // Top-align when bubbles are shown at the top or are max size.
545             return topAlignment;
546         }
547 
548         // If we're here, we're showing vertically & developer has made height less than maximum.
549         float pointerPosition = getPointerPosition(bubblePosition);
550         float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight;
551         float topIfCentered = pointerPosition - (expandedViewHeight / 2);
552         if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) {
553             // Center it
554             return pointerPosition - mPointerWidth - (expandedViewHeight / 2f);
555         } else if (topIfCentered <= mPositionRect.top) {
556             // Top align
557             return topAlignment;
558         } else {
559             // Bottom align
560             return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth;
561         }
562     }
563 
564     /**
565      * The position the pointer points to, the center of the bubble.
566      *
567      * @param bubblePosition the x position of the bubble if showing on top, the y position of the
568      *                       bubble if showing vertically.
569      * @return the position the tip of the pointer points to. The x position if showing on top, the
570      * y position if showing vertically.
571      */
getPointerPosition(float bubblePosition)572     public float getPointerPosition(float bubblePosition) {
573         // TODO: I don't understand why it works but it does - why normalized in portrait
574         //  & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
575         final float normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * getBubbleSize());
576         return showBubblesVertically()
577                 ? bubblePosition + (getBubbleSize() / 2f)
578                 : bubblePosition + (normalizedSize / 2f) - mPointerWidth;
579     }
580 
getExpandedStackSize(int numberOfBubbles)581     private int getExpandedStackSize(int numberOfBubbles) {
582         return (numberOfBubbles * mBubbleSize)
583                 + ((numberOfBubbles - 1) * mSpacingBetweenBubbles);
584     }
585 
586     /**
587      * Returns the position of the bubble on-screen when the stack is expanded.
588      *
589      * @param index the index of the bubble in the stack.
590      * @param state state information about the stack to help with calculations.
591      * @return the position of the bubble on-screen when the stack is expanded.
592      */
getExpandedBubbleXY(int index, BubbleStackView.StackViewState state)593     public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) {
594         boolean showBubblesVertically = showBubblesVertically();
595 
596         int onScreenIndex;
597         if (showBubblesVertically || !mDeviceConfig.isRtl()) {
598             onScreenIndex = index;
599         } else {
600             // If bubbles are shown horizontally, check if RTL language is used.
601             // If RTL is active, position first bubble on the right and last on the left.
602             // Last bubble has screen index 0 and first bubble has max screen index value.
603             onScreenIndex = state.numberOfBubbles - 1 - index;
604         }
605         final float positionInRow = onScreenIndex * (mBubbleSize + mSpacingBetweenBubbles);
606         final float rowStart = getBubbleRowStart(state);
607         float x;
608         float y;
609         if (showBubblesVertically) {
610             int inset = mExpandedViewLargeScreenInsetClosestEdge;
611             y = rowStart + positionInRow;
612             int left = mDeviceConfig.isLargeScreen()
613                     ? inset - mExpandedViewPadding - mBubbleSize
614                     : mPositionRect.left;
615             int right = mDeviceConfig.isLargeScreen()
616                     ? mPositionRect.right - inset + mExpandedViewPadding
617                     : mPositionRect.right - mBubbleSize;
618             x = state.onLeft
619                     ? left
620                     : right;
621         } else {
622             y = mPositionRect.top + mExpandedViewPadding;
623             x = rowStart + positionInRow;
624         }
625 
626         if (showBubblesVertically && mImeVisible) {
627             return new PointF(x, getExpandedBubbleYForIme(onScreenIndex, state));
628         }
629         return new PointF(x, y);
630     }
631 
getBubbleRowStart(BubbleStackView.StackViewState state)632     private float getBubbleRowStart(BubbleStackView.StackViewState state) {
633         final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
634         final float rowStart;
635         if (areBubblesBottomAligned()) {
636             final float expandedViewHeight = getExpandedViewHeightForLargeScreen();
637             final float expandedViewBottom = mScreenRect.bottom
638                     - Math.max(mInsets.bottom, mInsets.top)
639                     - mManageButtonHeight - mPointerWidth;
640             final float expandedViewCenter = expandedViewBottom - (expandedViewHeight / 2f);
641             rowStart = expandedViewCenter - (expandedStackSize / 2f);
642         } else {
643             final float centerPosition = showBubblesVertically()
644                     ? mPositionRect.centerY()
645                     : mPositionRect.centerX();
646             rowStart = centerPosition - (expandedStackSize / 2f);
647         }
648         return rowStart;
649     }
650 
651     /**
652      * Returns the position of the bubble on-screen when the stack is expanded and the IME
653      * is showing.
654      *
655      * @param index the index of the bubble in the stack.
656      * @param state information about the stack state (# of bubbles, selected bubble).
657      * @return y position of the bubble on-screen when the stack is expanded.
658      */
getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state)659     private float getExpandedBubbleYForIme(int index, BubbleStackView.StackViewState state) {
660         final float top = getAvailableRect().top + mExpandedViewPadding;
661         if (!showBubblesVertically()) {
662             // Showing horizontally: align to top
663             return top;
664         }
665 
666         // Showing vertically: might need to translate the bubbles above the IME.
667         // Add spacing here to provide a margin between top of IME and bottom of bubble row.
668         final float bottomHeight = getImeHeight() + mInsets.bottom + (mSpacingBetweenBubbles * 2);
669         final float bottomInset = mScreenRect.bottom - bottomHeight;
670         final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
671         final float rowTop = getBubbleRowStart(state);
672         final float rowBottom = rowTop + expandedStackSize;
673         float rowTopForIme = rowTop;
674         if (rowBottom > bottomInset) {
675             // We overlap with IME, must shift the bubbles
676             float translationY = rowBottom - bottomInset;
677             rowTopForIme = Math.max(rowTop - translationY, top);
678             if (rowTop - translationY < top) {
679                 // Even if we shift the bubbles, they will still overlap with the IME.
680                 // Hide the overflow for a lil more space:
681                 final float expandedStackSizeNoO = getExpandedStackSize(state.numberOfBubbles - 1);
682                 final float centerPositionNoO = showBubblesVertically()
683                         ? mPositionRect.centerY()
684                         : mPositionRect.centerX();
685                 final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f);
686                 final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f);
687                 translationY = rowBottomNoO - bottomInset;
688                 rowTopForIme = rowTopNoO - translationY;
689             }
690         }
691         // Check if the selected bubble is within the appropriate space
692         final float selectedPosition = rowTopForIme
693                 + (state.selectedIndex * (mBubbleSize + mSpacingBetweenBubbles));
694         if (selectedPosition < top) {
695             // We must always keep the selected bubble in view so we'll have to allow more overlap.
696             rowTopForIme = top;
697         }
698         return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles));
699     }
700 
701     /**
702      * @return the width of the bubble flyout (message originating from the bubble).
703      */
getMaxFlyoutSize()704     public float getMaxFlyoutSize() {
705         if (isLargeScreen()) {
706             return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN,
707                     mMinimumFlyoutWidthLargeScreen);
708         }
709         return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT;
710     }
711 
712     /**
713      * Returns the z translation a specific bubble should use. When expanded we keep a slight
714      * translation to ensure proper ordering when animating to / from collapsed state. When
715      * collapsed, only the top two bubbles appear so only their shadows show.
716      */
getZTranslation(int index, boolean isOverflow, boolean isExpanded)717     public float getZTranslation(int index, boolean isOverflow, boolean isExpanded) {
718         if (isOverflow) {
719             return 0f; // overflow is lowest
720         }
721         return isExpanded
722                 // When expanded use minimal amount to keep order
723                 ? getMaxBubbles() - index
724                 // When collapsed, only the top two bubbles have elevation
725                 : index < NUM_VISIBLE_WHEN_RESTING
726                         ? (getMaxBubbles() * mBubbleElevation) - index
727                         : 0;
728     }
729 
730     /** The elevation to use for bubble UI elements. */
getBubbleElevation()731     public int getBubbleElevation() {
732         return mBubbleElevation;
733     }
734 
735     /**
736      * @return whether the stack is considered on the left side of the screen.
737      */
isStackOnLeft(PointF currentStackPosition)738     public boolean isStackOnLeft(PointF currentStackPosition) {
739         if (currentStackPosition == null) {
740             currentStackPosition = getRestingPosition();
741         }
742         final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2;
743         return stackCenter < mScreenRect.width() / 2;
744     }
745 
746     /**
747      * Sets the stack's most recent position along the edge of the screen. This is saved when the
748      * last bubble is removed, so that the stack can be restored in its previous position.
749      */
setRestingPosition(PointF position)750     public void setRestingPosition(PointF position) {
751         if (mRestingStackPosition == null) {
752             mRestingStackPosition = new PointF(position);
753         } else {
754             mRestingStackPosition.set(position);
755         }
756     }
757 
758     /** The position the bubble stack should rest at when collapsed. */
getRestingPosition()759     public PointF getRestingPosition() {
760         if (mRestingStackPosition == null) {
761             return getDefaultStartPosition();
762         }
763         return mRestingStackPosition;
764     }
765 
766     /**
767      * Returns whether the {@link #getRestingPosition()} is equal to the default start position
768      * initialized for bubbles, if {@code true} this means the user hasn't moved the bubble
769      * from the initial start position (or they haven't received a bubble yet).
770      */
hasUserModifiedDefaultPosition()771     public boolean hasUserModifiedDefaultPosition() {
772         PointF defaultStart = getDefaultStartPosition();
773         return mRestingStackPosition != null
774                 && !mRestingStackPosition.equals(defaultStart);
775     }
776 
777     /**
778      * Returns the stack position to use if we don't have a saved location or if user education
779      * is being shown, for a normal bubble.
780      */
getDefaultStartPosition()781     public PointF getDefaultStartPosition() {
782         return getDefaultStartPosition(false /* isNoteBubble */);
783     }
784 
785     /**
786      * The stack position to use if we don't have a saved location or if user education
787      * is being shown.
788      *
789      * @param isNoteBubble whether this start position is for a note bubble or not.
790      */
getDefaultStartPosition(boolean isNoteBubble)791     public PointF getDefaultStartPosition(boolean isNoteBubble) {
792         // Normal bubbles start on the left if we're in LTR, right otherwise.
793         // TODO (b/294284894): update language around "app bubble" here
794         // App bubbles start on the right in RTL, left otherwise.
795         final boolean startOnLeft = isNoteBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl();
796         return getStartPosition(startOnLeft ? StackPinnedEdge.LEFT : StackPinnedEdge.RIGHT);
797     }
798 
799     /**
800      * The stack position to use if user education is being shown.
801      *
802      * @param stackPinnedEdge the screen edge the stack is pinned to.
803      */
getStartPosition(StackPinnedEdge stackPinnedEdge)804     public PointF getStartPosition(StackPinnedEdge stackPinnedEdge) {
805         final RectF allowableStackPositionRegion = getAllowableStackPositionRegion(
806                 1 /* default starts with 1 bubble */);
807         if (isLargeScreen()) {
808             // We want the stack to be visually centered on the edge, so we need to base it
809             // of a rect that includes insets.
810             final float desiredY = mScreenRect.height() / 2f - (mBubbleSize / 2f);
811             final float offset = desiredY / mScreenRect.height();
812             return new BubbleStackView.RelativeStackPosition(
813                     stackPinnedEdge == StackPinnedEdge.LEFT,
814                     offset)
815                     .getAbsolutePositionInRegion(allowableStackPositionRegion);
816         } else {
817             final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset(
818                     R.dimen.bubble_stack_starting_offset_y);
819             // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge
820             return new BubbleStackView.RelativeStackPosition(
821                     stackPinnedEdge == StackPinnedEdge.LEFT,
822                     startingVerticalOffset / mPositionRect.height())
823                     .getAbsolutePositionInRegion(allowableStackPositionRegion);
824         }
825     }
826 
827     /**
828      * Returns the region that the stack position must stay within. This goes slightly off the left
829      * and right sides of the screen, below the status bar/cutout and above the navigation bar.
830      * While the stack position is not allowed to rest outside of these bounds, it can temporarily
831      * be animated or dragged beyond them.
832      */
getAllowableStackPositionRegion(int bubbleCount)833     public RectF getAllowableStackPositionRegion(int bubbleCount) {
834         final RectF allowableRegion = new RectF(getAvailableRect());
835         final int imeHeight = getImeHeight();
836         final float bottomPadding = bubbleCount > 1
837                 ? mBubblePaddingTop + mStackOffset
838                 : mBubblePaddingTop;
839         allowableRegion.left -= mBubbleOffscreenAmount;
840         allowableRegion.top += mBubblePaddingTop;
841         allowableRegion.right += mBubbleOffscreenAmount - mBubbleSize;
842         allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize;
843         return allowableRegion;
844     }
845 
846     /**
847      * Navigation bar has an area where system gestures can be started from.
848      *
849      * @return {@link Rect} for system navigation bar gesture zone
850      */
getNavBarGestureZone()851     public Rect getNavBarGestureZone() {
852         // Gesture zone height from the bottom
853         int gestureZoneHeight = mContext.getResources().getDimensionPixelSize(
854                 com.android.internal.R.dimen.navigation_bar_gesture_height);
855         Rect screen = getScreenRect();
856         return new Rect(
857                 screen.left,
858                 screen.bottom - gestureZoneHeight,
859                 screen.right,
860                 screen.bottom);
861     }
862 
863     //
864     // Bubble bar specific sizes below.
865     //
866 
867     /**
868      * Sets whether bubbles are showing in the bubble bar from launcher.
869      */
setShowingInBubbleBar(boolean showingInBubbleBar)870     public void setShowingInBubbleBar(boolean showingInBubbleBar) {
871         mShowingInBubbleBar = showingInBubbleBar;
872     }
873 
874     /**
875      * Whether bubbles ar showing in the bubble bar from launcher.
876      */
isShowingInBubbleBar()877     boolean isShowingInBubbleBar() {
878         return mShowingInBubbleBar;
879     }
880 
setBubbleBarLocation(BubbleBarLocation location)881     public void setBubbleBarLocation(BubbleBarLocation location) {
882         mBubbleBarLocation = location;
883     }
884 
getBubbleBarLocation()885     public BubbleBarLocation getBubbleBarLocation() {
886         return mBubbleBarLocation;
887     }
888 
889     /**
890      * @return <code>true</code> when bubble bar is on the left and <code>false</code> when on right
891      */
isBubbleBarOnLeft()892     public boolean isBubbleBarOnLeft() {
893         return mBubbleBarLocation.isOnLeft(mDeviceConfig.isRtl());
894     }
895 
896     /**
897      * Set top coordinate of bubble bar on screen
898      */
setBubbleBarTopOnScreen(int topOnScreen)899     public void setBubbleBarTopOnScreen(int topOnScreen) {
900         mBubbleBarTopOnScreen = topOnScreen;
901     }
902 
903     /**
904      * Returns the top coordinate of bubble bar on screen
905      */
getBubbleBarTopOnScreen()906     public int getBubbleBarTopOnScreen() {
907         return mBubbleBarTopOnScreen;
908     }
909 
910     /**
911      * How wide the expanded view should be when showing from the bubble bar.
912      */
getExpandedViewWidthForBubbleBar(boolean isOverflow)913     public int getExpandedViewWidthForBubbleBar(boolean isOverflow) {
914         return isOverflow ? mOverflowWidth : mExpandedViewBubbleBarWidth;
915     }
916 
917     /**
918      * How tall the expanded view should be when showing from the bubble bar.
919      */
getExpandedViewHeightForBubbleBar(boolean isOverflow)920     public int getExpandedViewHeightForBubbleBar(boolean isOverflow) {
921         if (isOverflow) {
922             return mOverflowHeight;
923         } else {
924             return getBubbleBarExpandedViewHeight();
925         }
926     }
927 
928     /**
929      * Calculate the height of expanded view in landscape mode regardless current orientation.
930      * Here is an explanation:
931      * ------------------------ mScreenRect.top
932      * |         top inset ↕  |
933      * |-----------------------
934      * |      16dp spacing ↕  |
935      * |           ---------  | --- expanded view top
936      * |           |       |  |   ↑
937      * |           |       |  |   ↓ expanded view height
938      * |           ---------  | --- expanded view bottom
939      * |      16dp spacing ↕  |   ↑
940      * |         @bubble bar@ |   | height of the bubble bar container
941      * ------------------------   | already includes bottom inset and spacing
942      * |      bottom inset ↕  |   ↓
943      * |----------------------| --- mScreenRect.bottom
944      */
getBubbleBarExpandedViewHeight()945     private int getBubbleBarExpandedViewHeight() {
946         int heightOfBubbleBarContainer =
947                 mScreenRect.height() - getExpandedViewBottomForBubbleBar();
948         int expandedViewHeight;
949         if (Flags.enableBubbleBarOnPhones() && !mDeviceConfig.isLargeScreen()) {
950             // we're on a phone, use the max / height
951             expandedViewHeight = Math.max(mScreenRect.width(), mScreenRect.height());
952         } else {
953             // getting landscape height from screen rect
954             expandedViewHeight = Math.min(mScreenRect.width(), mScreenRect.height());
955         }
956         expandedViewHeight -= heightOfBubbleBarContainer; /* removing bubble container height */
957         expandedViewHeight -= mInsets.top; /* removing top inset */
958         expandedViewHeight -= mExpandedViewPadding; /* removing spacing */
959         return expandedViewHeight;
960     }
961 
962     /** The bottom position of the expanded view when showing above the bubble bar. */
getExpandedViewBottomForBubbleBar()963     public int getExpandedViewBottomForBubbleBar() {
964         return mBubbleBarTopOnScreen - mExpandedViewPadding;
965     }
966 
967     /**
968      * The amount of padding from the edge of the screen to the expanded view when in bubble bar.
969      */
getBubbleBarExpandedViewPadding()970     public int getBubbleBarExpandedViewPadding() {
971         return mExpandedViewPadding;
972     }
973 
974     /**
975      * Get bubble bar expanded view bounds on screen
976      */
getBubbleBarExpandedViewBounds(boolean onLeft, boolean isOverflowExpanded, Rect out)977     public void getBubbleBarExpandedViewBounds(boolean onLeft, boolean isOverflowExpanded,
978             Rect out) {
979         final int padding = getBubbleBarExpandedViewPadding();
980         final int width = getExpandedViewWidthForBubbleBar(isOverflowExpanded);
981         final int height = getExpandedViewHeightForBubbleBar(isOverflowExpanded);
982 
983         out.set(0, 0, width, height);
984         int left;
985         if (onLeft) {
986             left = getInsets().left + padding;
987         } else {
988             left = getAvailableRect().right - width - padding;
989         }
990         int top = getExpandedViewBottomForBubbleBar() - height;
991         out.offsetTo(left, top);
992     }
993 
994     @NonNull
995     @Override
getBubbleBarExpandedViewDropTargetBounds(boolean onLeft)996     public Rect getBubbleBarExpandedViewDropTargetBounds(boolean onLeft) {
997         Rect bounds = new Rect();
998         getBubbleBarExpandedViewBounds(onLeft, false, bounds);
999         // Drop target bounds are based on expanded view bounds with some padding added
1000         int leftPadding = onLeft ? 0 : mBarExpViewDropTargetPaddingHorizontal;
1001         int rightPadding = onLeft ? mBarExpViewDropTargetPaddingHorizontal : 0;
1002         bounds.inset(
1003                 leftPadding,
1004                 mBarExpViewDropTargetPaddingTop,
1005                 rightPadding,
1006                 mBarExpViewDropTargetPaddingBottom
1007         );
1008         return bounds;
1009     }
1010 
1011     @NonNull
1012     @Override
getBarDropTargetBounds(boolean onLeft)1013     public Rect getBarDropTargetBounds(boolean onLeft) {
1014         Rect bounds = getBubbleBarExpandedViewDropTargetBounds(onLeft);
1015         bounds.top = getBubbleBarTopOnScreen();
1016         bounds.bottom = bounds.top + mBarDropTargetHeight;
1017         if (onLeft) {
1018             // Keep the left edge from expanded view
1019             bounds.right = bounds.left + mBarDropTargetWidth;
1020         } else {
1021             // Keep the right edge from expanded view
1022             bounds.left = bounds.right - mBarDropTargetWidth;
1023         }
1024         return bounds;
1025     }
1026 }
1027