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