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 java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.IntDef;
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.util.Log;
29 import android.view.Surface;
30 import android.view.View;
31 import android.view.WindowInsets;
32 import android.view.WindowManager;
33 import android.view.WindowMetrics;
34 
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.launcher3.icons.IconNormalizer;
38 import com.android.wm.shell.R;
39 
40 import java.lang.annotation.Retention;
41 
42 /**
43  * Keeps track of display size, configuration, and specific bubble sizes. One place for all
44  * placement and positioning calculations to refer to.
45  */
46 public class BubblePositioner {
47     private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME
48             ? "BubblePositioner"
49             : BubbleDebugConfig.TAG_BUBBLES;
50 
51     @Retention(SOURCE)
52     @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT,
53             TASKBAR_POSITION_BOTTOM})
54     @interface TaskbarPosition {}
55     public static final int TASKBAR_POSITION_NONE = -1;
56     public static final int TASKBAR_POSITION_RIGHT = 0;
57     public static final int TASKBAR_POSITION_LEFT = 1;
58     public static final int TASKBAR_POSITION_BOTTOM = 2;
59 
60     /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
61     public static final int NUM_VISIBLE_WHEN_RESTING = 2;
62     /** Indicates a bubble's height should be the maximum available space. **/
63     public static final int MAX_HEIGHT = -1;
64     /** The max percent of screen width to use for the flyout on large screens. */
65     public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f;
66     /** The max percent of screen width to use for the flyout on phone. */
67     public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f;
68     /** The percent of screen width that should be used for the expanded view on a large screen. **/
69     public static final float EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT = 0.72f;
70 
71     private Context mContext;
72     private WindowManager mWindowManager;
73     private Rect mScreenRect;
74     private @Surface.Rotation int mRotation = Surface.ROTATION_0;
75     private Insets mInsets;
76     private boolean mImeVisible;
77     private int mImeHeight;
78     private boolean mIsLargeScreen;
79 
80     private Rect mPositionRect;
81     private int mDefaultMaxBubbles;
82     private int mMaxBubbles;
83     private int mBubbleSize;
84     private int mSpacingBetweenBubbles;
85 
86     private int mExpandedViewMinHeight;
87     private int mExpandedViewLargeScreenWidth;
88     private int mExpandedViewLargeScreenInset;
89 
90     private int mOverflowWidth;
91     private int mExpandedViewPadding;
92     private int mPointerMargin;
93     private int mPointerWidth;
94     private int mPointerHeight;
95     private int mPointerOverlap;
96     private int mManageButtonHeight;
97     private int mOverflowHeight;
98     private int mMinimumFlyoutWidthLargeScreen;
99 
100     private PointF mPinLocation;
101     private PointF mRestingStackPosition;
102     private int[] mPaddings = new int[4];
103 
104     private boolean mShowingInTaskbar;
105     private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE;
106     private int mTaskbarIconSize;
107     private int mTaskbarSize;
108 
BubblePositioner(Context context, WindowManager windowManager)109     public BubblePositioner(Context context, WindowManager windowManager) {
110         mContext = context;
111         mWindowManager = windowManager;
112         update();
113     }
114 
setRotation(int rotation)115     public void setRotation(int rotation) {
116         mRotation = rotation;
117     }
118 
119     /**
120      * Available space and inset information. Call this when config changes
121      * occur or when added to a window.
122      */
update()123     public void update() {
124         WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
125         if (windowMetrics == null) {
126             return;
127         }
128         WindowInsets metricInsets = windowMetrics.getWindowInsets();
129         Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
130                 | WindowInsets.Type.statusBars()
131                 | WindowInsets.Type.displayCutout());
132 
133         mIsLargeScreen = mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600;
134 
135         if (BubbleDebugConfig.DEBUG_POSITIONER) {
136             Log.w(TAG, "update positioner:"
137                     + " rotation: " + mRotation
138                     + " insets: " + insets
139                     + " isLargeScreen: " + mIsLargeScreen
140                     + " bounds: " + windowMetrics.getBounds()
141                     + " showingInTaskbar: " + mShowingInTaskbar);
142         }
143         updateInternal(mRotation, insets, windowMetrics.getBounds());
144     }
145 
146     /**
147      * Updates position information to account for taskbar state.
148      *
149      * @param taskbarPosition which position the taskbar is displayed in.
150      * @param showingInTaskbar whether the taskbar is being shown.
151      */
updateForTaskbar(int iconSize, @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize)152     public void updateForTaskbar(int iconSize,
153             @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) {
154         mShowingInTaskbar = showingInTaskbar;
155         mTaskbarIconSize =  iconSize;
156         mTaskbarPosition = taskbarPosition;
157         mTaskbarSize = taskbarSize;
158         update();
159     }
160 
161     @VisibleForTesting
updateInternal(int rotation, Insets insets, Rect bounds)162     public void updateInternal(int rotation, Insets insets, Rect bounds) {
163         mRotation = rotation;
164         mInsets = insets;
165 
166         mScreenRect = new Rect(bounds);
167         mPositionRect = new Rect(bounds);
168         mPositionRect.left += mInsets.left;
169         mPositionRect.top += mInsets.top;
170         mPositionRect.right -= mInsets.right;
171         mPositionRect.bottom -= mInsets.bottom;
172 
173         Resources res = mContext.getResources();
174         mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
175         mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
176         mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
177         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
178         mExpandedViewLargeScreenWidth = (int) (bounds.width()
179                 * EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT);
180         mExpandedViewLargeScreenInset = mIsLargeScreen
181                 ? (bounds.width() - mExpandedViewLargeScreenWidth) / 2
182                 : mExpandedViewPadding;
183         mOverflowWidth = mIsLargeScreen
184                 ? mExpandedViewLargeScreenWidth
185                 : res.getDimensionPixelSize(
186                         R.dimen.bubble_expanded_view_phone_landscape_overflow_width);
187         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
188         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
189         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
190         mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
191         mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height);
192         mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
193         mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
194         mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize(
195                 R.dimen.bubbles_flyout_min_width_large_screen);
196 
197         mMaxBubbles = calculateMaxBubbles();
198 
199         if (mShowingInTaskbar) {
200             adjustForTaskbar();
201         }
202     }
203 
204     /**
205      * @return the maximum number of bubbles that can fit on the screen when expanded. If the
206      * screen size / screen density is too small to support the default maximum number, then
207      * the number will be adjust to something lower to ensure everything is presented nicely.
208      */
calculateMaxBubbles()209     private int calculateMaxBubbles() {
210         // Use the shortest edge.
211         // In portrait the bubbles should align with the expanded view so subtract its padding.
212         // We always show the overflow so subtract one bubble size.
213         int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2);
214         int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height())
215                 - padding
216                 - mBubbleSize;
217         // Each of the bubbles have spacing because the overflow is at the end.
218         int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles);
219         if (howManyFit < mDefaultMaxBubbles) {
220             // Not enough space for the default.
221             return howManyFit;
222         }
223         return mDefaultMaxBubbles;
224     }
225 
226     /**
227      * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should
228      * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space
229      * and insets to account for the taskbar.
230      */
231     // TODO(b/171559950): When the insets are reported correctly we can remove this logic
adjustForTaskbar()232     private void adjustForTaskbar() {
233         // When bar is showing on edges... subtract that inset because we appear on top
234         if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) {
235             WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
236             Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility(
237                     WindowInsets.Type.navigationBars());
238             int newInsetLeft = mInsets.left;
239             int newInsetRight = mInsets.right;
240             if (mTaskbarPosition == TASKBAR_POSITION_LEFT) {
241                 mPositionRect.left -= navBarInsets.left;
242                 newInsetLeft -= navBarInsets.left;
243             } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) {
244                 mPositionRect.right += navBarInsets.right;
245                 newInsetRight -= navBarInsets.right;
246             }
247             mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom);
248         }
249     }
250 
251     /**
252      * @return a rect of available screen space accounting for orientation, system bars and cutouts.
253      * Does not account for IME.
254      */
getAvailableRect()255     public Rect getAvailableRect() {
256         return mPositionRect;
257     }
258 
259     /**
260      * @return a rect of the screen size.
261      */
getScreenRect()262     public Rect getScreenRect() {
263         return mScreenRect;
264     }
265 
266     /**
267      * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
268      * inset is not included here.
269      */
getInsets()270     public Insets getInsets() {
271         return mInsets;
272     }
273 
274     /** @return whether the device is in landscape orientation. */
isLandscape()275     public boolean isLandscape() {
276         return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
277     }
278 
279     /** @return whether the screen is considered large. */
isLargeScreen()280     public boolean isLargeScreen() {
281         return mIsLargeScreen;
282     }
283 
284     /**
285      * Indicates how bubbles appear when expanded.
286      *
287      * When false, bubbles display at the top of the screen with the expanded view
288      * below them. When true, bubbles display at the edges of the screen with the expanded view
289      * to the left or right side.
290      */
showBubblesVertically()291     public boolean showBubblesVertically() {
292         return isLandscape() || mShowingInTaskbar || mIsLargeScreen;
293     }
294 
295     /** Size of the bubble. */
getBubbleSize()296     public int getBubbleSize() {
297         return (mShowingInTaskbar && mTaskbarIconSize > 0)
298                 ? mTaskbarIconSize
299                 : mBubbleSize;
300     }
301 
302     /** The maximum number of bubbles that can be displayed comfortably on screen. */
getMaxBubbles()303     public int getMaxBubbles() {
304         return mMaxBubbles;
305     }
306 
307     /** The height for the IME if it's visible. **/
getImeHeight()308     public int getImeHeight() {
309         return mImeVisible ? mImeHeight : 0;
310     }
311 
312     /** Sets whether the IME is visible. **/
setImeVisible(boolean visible, int height)313     public void setImeVisible(boolean visible, int height) {
314         mImeVisible = visible;
315         mImeHeight = height;
316     }
317 
318     /**
319      * Calculates the padding for the bubble expanded view.
320      *
321      * Some specifics:
322      * On large screens the width of the expanded view is restricted via this padding.
323      * On phone landscape the bubble overflow expanded view is also restricted via this padding.
324      * On large screens & landscape no top padding is set, the top position is set via translation.
325      * On phone portrait top padding is set as the space between the tip of the pointer and the
326      * bubble.
327      * When the overflow is shown it doesn't have the manage button to pad out the bottom so
328      * padding is added.
329      */
getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow)330     public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) {
331         final int pointerTotalHeight = mPointerHeight - mPointerOverlap;
332         if (mIsLargeScreen) {
333             // [left, top, right, bottom]
334             mPaddings[0] = onLeft
335                     ? mExpandedViewLargeScreenInset - pointerTotalHeight
336                     : mExpandedViewLargeScreenInset;
337             mPaddings[1] = 0;
338             mPaddings[2] = onLeft
339                     ? mExpandedViewLargeScreenInset
340                     : mExpandedViewLargeScreenInset - pointerTotalHeight;
341             // Overflow doesn't show manage button / get padding from it so add padding here for it
342             mPaddings[3] = isOverflow ? mExpandedViewPadding : 0;
343             return mPaddings;
344         } else {
345             int leftPadding = mInsets.left + mExpandedViewPadding;
346             int rightPadding = mInsets.right + mExpandedViewPadding;
347             final float expandedViewWidth = isOverflow
348                     ? mOverflowWidth
349                     : mExpandedViewLargeScreenWidth;
350             if (showBubblesVertically()) {
351                 if (!onLeft) {
352                     rightPadding += mBubbleSize - pointerTotalHeight;
353                     leftPadding += isOverflow
354                             ? (mPositionRect.width() - rightPadding - expandedViewWidth)
355                             : 0;
356                 } else {
357                     leftPadding += mBubbleSize - pointerTotalHeight;
358                     rightPadding += isOverflow
359                             ? (mPositionRect.width() - leftPadding - expandedViewWidth)
360                             : 0;
361                 }
362             }
363             // [left, top, right, bottom]
364             mPaddings[0] = leftPadding;
365             mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
366             mPaddings[2] = rightPadding;
367             mPaddings[3] = 0;
368             return mPaddings;
369         }
370     }
371 
372     /** Gets the y position of the expanded view if it was top-aligned. */
getExpandedViewYTopAligned()373     public float getExpandedViewYTopAligned() {
374         final int top = getAvailableRect().top;
375         if (showBubblesVertically()) {
376             return top - mPointerWidth + mExpandedViewPadding;
377         } else {
378             return top + mBubbleSize + mPointerMargin;
379         }
380     }
381 
382     /**
383      * Calculate the maximum height the expanded view can be depending on where it's placed on
384      * the screen and the size of the elements around it (e.g. padding, pointer, manage button).
385      */
getMaxExpandedViewHeight(boolean isOverflow)386     public int getMaxExpandedViewHeight(boolean isOverflow) {
387         // Subtract top insets because availableRect.height would account for that
388         int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top;
389         int paddingTop = showBubblesVertically()
390                 ? 0
391                 : mPointerHeight;
392         // Subtract pointer size because it's laid out in LinearLayout with the expanded view.
393         int pointerSize = showBubblesVertically()
394                 ? mPointerWidth
395                 : (mPointerHeight + mPointerMargin);
396         int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
397         return getAvailableRect().height()
398                 - expandedContainerY
399                 - paddingTop
400                 - pointerSize
401                 - bottomPadding;
402     }
403 
404     /**
405      * Determines the height for the bubble, ensuring a minimum height. If the height should be as
406      * big as available, returns {@link #MAX_HEIGHT}.
407      */
getExpandedViewHeight(BubbleViewProvider bubble)408     public float getExpandedViewHeight(BubbleViewProvider bubble) {
409         boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
410         if (isOverflow && showBubblesVertically() && !mIsLargeScreen) {
411             // overflow in landscape on phone is max
412             return MAX_HEIGHT;
413         }
414         float desiredHeight = isOverflow
415                 ? mOverflowHeight
416                 : ((Bubble) bubble).getDesiredHeight(mContext);
417         desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight);
418         if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) {
419             return MAX_HEIGHT;
420         }
421         return desiredHeight;
422     }
423 
424     /**
425      * Gets the y position for the expanded view. This is the position on screen of the top
426      * horizontal line of the expanded view.
427      *
428      * @param bubble the bubble being positioned.
429      * @param bubblePosition the x position of the bubble if showing on top, the y position of the
430      *                       bubble if showing vertically.
431      * @return the y position for the expanded view.
432      */
getExpandedViewY(BubbleViewProvider bubble, float bubblePosition)433     public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) {
434         boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey());
435         float expandedViewHeight = getExpandedViewHeight(bubble);
436         float topAlignment = getExpandedViewYTopAligned();
437         if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) {
438             // Top-align when bubbles are shown at the top or are max size.
439             return topAlignment;
440         }
441         // If we're here, we're showing vertically & developer has made height less than maximum.
442         int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight;
443         float pointerPosition = getPointerPosition(bubblePosition);
444         float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight;
445         float topIfCentered = pointerPosition - (expandedViewHeight / 2);
446         if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) {
447             // Center it
448             return pointerPosition - mPointerWidth - (expandedViewHeight / 2f);
449         } else if (topIfCentered <= mPositionRect.top) {
450             // Top align
451             return topAlignment;
452         } else {
453             // Bottom align
454             return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth;
455         }
456     }
457 
458     /**
459      * The position the pointer points to, the center of the bubble.
460      *
461      * @param bubblePosition the x position of the bubble if showing on top, the y position of the
462      *                       bubble if showing vertically.
463      * @return the position the tip of the pointer points to. The x position if showing on top, the
464      * y position if showing vertically.
465      */
getPointerPosition(float bubblePosition)466     public float getPointerPosition(float bubblePosition) {
467         // TODO: I don't understand why it works but it does - why normalized in portrait
468         //  & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
469         final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
470                 getBubbleSize());
471         return showBubblesVertically()
472                 ? bubblePosition + (getBubbleSize() / 2f)
473                 : bubblePosition + (normalizedSize / 2f) - mPointerWidth;
474     }
475 
getExpandedStackSize(int numberOfBubbles)476     private int getExpandedStackSize(int numberOfBubbles) {
477         return (numberOfBubbles * mBubbleSize)
478                 + ((numberOfBubbles - 1) * mSpacingBetweenBubbles);
479     }
480 
481     /**
482      * Returns the position of the bubble on-screen when the stack is expanded.
483      *
484      * @param index the index of the bubble in the stack.
485      * @param state state information about the stack to help with calculations.
486      * @return the position of the bubble on-screen when the stack is expanded.
487      */
getExpandedBubbleXY(int index, BubbleStackView.StackViewState state)488     public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) {
489         final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles);
490         final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles);
491         final float centerPosition = showBubblesVertically()
492                 ? mPositionRect.centerY()
493                 : mPositionRect.centerX();
494         // alignment - centered on the edge
495         final float rowStart = centerPosition - (expandedStackSize / 2f);
496         float x;
497         float y;
498         if (showBubblesVertically()) {
499             y = rowStart + positionInRow;
500             int left = mIsLargeScreen
501                     ? mExpandedViewLargeScreenInset - mExpandedViewPadding - mBubbleSize
502                     : mPositionRect.left;
503             int right = mIsLargeScreen
504                     ? mPositionRect.right - mExpandedViewLargeScreenInset + mExpandedViewPadding
505                     : mPositionRect.right - mBubbleSize;
506             x = state.onLeft
507                     ? left
508                     : right;
509         } else {
510             y = mPositionRect.top + mExpandedViewPadding;
511             x = rowStart + positionInRow;
512         }
513 
514         if (showBubblesVertically() && mImeVisible) {
515             return new PointF(x, getExpandedBubbleYForIme(index, state.numberOfBubbles));
516         }
517         return new PointF(x, y);
518     }
519 
520     /**
521      * Returns the position of the bubble on-screen when the stack is expanded and the IME
522      * is showing.
523      *
524      * @param index the index of the bubble in the stack.
525      * @param numberOfBubbles the total number of bubbles in the stack.
526      * @return y position of the bubble on-screen when the stack is expanded.
527      */
getExpandedBubbleYForIme(int index, int numberOfBubbles)528     private float getExpandedBubbleYForIme(int index, int numberOfBubbles) {
529         final float top = getAvailableRect().top + mExpandedViewPadding;
530         if (!showBubblesVertically()) {
531             // Showing horizontally: align to top
532             return top;
533         }
534 
535         // Showing vertically: might need to translate the bubbles above the IME.
536         // Subtract spacing here to provide a margin between top of IME and bottom of bubble row.
537         final float bottomInset = getImeHeight() + mInsets.bottom - (mSpacingBetweenBubbles * 2);
538         final float expandedStackSize = getExpandedStackSize(numberOfBubbles);
539         final float centerPosition = showBubblesVertically()
540                 ? mPositionRect.centerY()
541                 : mPositionRect.centerX();
542         final float rowBottom = centerPosition + (expandedStackSize / 2f);
543         final float rowTop = centerPosition - (expandedStackSize / 2f);
544         float rowTopForIme = rowTop;
545         if (rowBottom > bottomInset) {
546             // We overlap with IME, must shift the bubbles
547             float translationY = rowBottom - bottomInset;
548             rowTopForIme = Math.max(rowTop - translationY, top);
549             if (rowTop - translationY < top) {
550                 // Even if we shift the bubbles, they will still overlap with the IME.
551                 // Hide the overflow for a lil more space:
552                 final float expandedStackSizeNoO = getExpandedStackSize(numberOfBubbles - 1);
553                 final float centerPositionNoO = showBubblesVertically()
554                         ? mPositionRect.centerY()
555                         : mPositionRect.centerX();
556                 final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f);
557                 final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f);
558                 translationY = rowBottomNoO - bottomInset;
559                 rowTopForIme = rowTopNoO - translationY;
560             }
561         }
562         return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles));
563     }
564 
565     /**
566      * @return the width of the bubble flyout (message originating from the bubble).
567      */
getMaxFlyoutSize()568     public float getMaxFlyoutSize() {
569         if (isLargeScreen()) {
570             return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN,
571                     mMinimumFlyoutWidthLargeScreen);
572         }
573         return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT;
574     }
575 
576     /**
577      * @return whether the stack is considered on the left side of the screen.
578      */
isStackOnLeft(PointF currentStackPosition)579     public boolean isStackOnLeft(PointF currentStackPosition) {
580         if (currentStackPosition == null) {
581             currentStackPosition = getRestingPosition();
582         }
583         final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2;
584         return stackCenter < mScreenRect.width() / 2;
585     }
586 
587     /**
588      * Sets the stack's most recent position along the edge of the screen. This is saved when the
589      * last bubble is removed, so that the stack can be restored in its previous position.
590      */
setRestingPosition(PointF position)591     public void setRestingPosition(PointF position) {
592         if (mRestingStackPosition == null) {
593             mRestingStackPosition = new PointF(position);
594         } else {
595             mRestingStackPosition.set(position);
596         }
597     }
598 
599     /** The position the bubble stack should rest at when collapsed. */
getRestingPosition()600     public PointF getRestingPosition() {
601         if (mPinLocation != null) {
602             return mPinLocation;
603         }
604         if (mRestingStackPosition == null) {
605             return getDefaultStartPosition();
606         }
607         return mRestingStackPosition;
608     }
609 
610     /**
611      * @return the stack position to use if we don't have a saved location or if user education
612      * is being shown.
613      */
getDefaultStartPosition()614     public PointF getDefaultStartPosition() {
615         // Start on the left if we're in LTR, right otherwise.
616         final boolean startOnLeft =
617                 mContext.getResources().getConfiguration().getLayoutDirection()
618                         != View.LAYOUT_DIRECTION_RTL;
619         final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset(
620                 R.dimen.bubble_stack_starting_offset_y);
621         // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge
622         return new BubbleStackView.RelativeStackPosition(
623                 startOnLeft,
624                 startingVerticalOffset / mPositionRect.height())
625                 .getAbsolutePositionInRegion(new RectF(mPositionRect));
626     }
627 
628     /**
629      * @return whether the bubble stack is pinned to the taskbar.
630      */
showingInTaskbar()631     public boolean showingInTaskbar() {
632         return mShowingInTaskbar;
633     }
634 
635     /**
636      * @return the taskbar position if set.
637      */
getTaskbarPosition()638     public int getTaskbarPosition() {
639         return mTaskbarPosition;
640     }
641 
getTaskbarSize()642     public int getTaskbarSize() {
643         return mTaskbarSize;
644     }
645 
646     /**
647      * In some situations bubbles will be pinned to a specific onscreen location. This sets the
648      * location to anchor the stack to.
649      */
setPinnedLocation(PointF point)650     public void setPinnedLocation(PointF point) {
651         mPinLocation = point;
652     }
653 }
654