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