• 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 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.wm.shell.R;
38 
39 import java.lang.annotation.Retention;
40 
41 /**
42  * Keeps track of display size, configuration, and specific bubble sizes. One place for all
43  * placement and positioning calculations to refer to.
44  */
45 public class BubblePositioner {
46     private static final String TAG = BubbleDebugConfig.TAG_WITH_CLASS_NAME
47             ? "BubblePositioner"
48             : BubbleDebugConfig.TAG_BUBBLES;
49 
50     @Retention(SOURCE)
51     @IntDef({TASKBAR_POSITION_NONE, TASKBAR_POSITION_RIGHT, TASKBAR_POSITION_LEFT,
52             TASKBAR_POSITION_BOTTOM})
53     @interface TaskbarPosition {}
54     public static final int TASKBAR_POSITION_NONE = -1;
55     public static final int TASKBAR_POSITION_RIGHT = 0;
56     public static final int TASKBAR_POSITION_LEFT = 1;
57     public static final int TASKBAR_POSITION_BOTTOM = 2;
58 
59     /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/
60     public static final int NUM_VISIBLE_WHEN_RESTING = 2;
61 
62     private Context mContext;
63     private WindowManager mWindowManager;
64     private Rect mPositionRect;
65     private @Surface.Rotation int mRotation = Surface.ROTATION_0;
66     private Insets mInsets;
67     private int mDefaultMaxBubbles;
68     private int mMaxBubbles;
69 
70     private int mBubbleSize;
71     private int mBubbleBadgeSize;
72     private int mSpacingBetweenBubbles;
73     private int mExpandedViewLargeScreenWidth;
74     private int mExpandedViewPadding;
75     private int mPointerMargin;
76     private float mPointerWidth;
77     private float mPointerHeight;
78 
79     private PointF mPinLocation;
80     private PointF mRestingStackPosition;
81     private int[] mPaddings = new int[4];
82 
83     private boolean mIsLargeScreen;
84     private boolean mShowingInTaskbar;
85     private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE;
86     private int mTaskbarIconSize;
87     private int mTaskbarSize;
88 
BubblePositioner(Context context, WindowManager windowManager)89     public BubblePositioner(Context context, WindowManager windowManager) {
90         mContext = context;
91         mWindowManager = windowManager;
92         update();
93     }
94 
setRotation(int rotation)95     public void setRotation(int rotation) {
96         mRotation = rotation;
97     }
98 
99     /**
100      * Available space and inset information. Call this when config changes
101      * occur or when added to a window.
102      */
update()103     public void update() {
104         WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
105         if (windowMetrics == null) {
106             return;
107         }
108         WindowInsets metricInsets = windowMetrics.getWindowInsets();
109         Insets insets = metricInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
110                 | WindowInsets.Type.statusBars()
111                 | WindowInsets.Type.displayCutout());
112 
113         mIsLargeScreen = mContext.getResources().getConfiguration().smallestScreenWidthDp >= 600;
114 
115         if (BubbleDebugConfig.DEBUG_POSITIONER) {
116             Log.w(TAG, "update positioner:"
117                     + " rotation: " + mRotation
118                     + " insets: " + insets
119                     + " isLargeScreen: " + mIsLargeScreen
120                     + " bounds: " + windowMetrics.getBounds()
121                     + " showingInTaskbar: " + mShowingInTaskbar);
122         }
123         updateInternal(mRotation, insets, windowMetrics.getBounds());
124     }
125 
126     /**
127      * Updates position information to account for taskbar state.
128      *
129      * @param taskbarPosition which position the taskbar is displayed in.
130      * @param showingInTaskbar whether the taskbar is being shown.
131      */
updateForTaskbar(int iconSize, @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize)132     public void updateForTaskbar(int iconSize,
133             @TaskbarPosition int taskbarPosition, boolean showingInTaskbar, int taskbarSize) {
134         mShowingInTaskbar = showingInTaskbar;
135         mTaskbarIconSize =  iconSize;
136         mTaskbarPosition = taskbarPosition;
137         mTaskbarSize = taskbarSize;
138         update();
139     }
140 
141     @VisibleForTesting
updateInternal(int rotation, Insets insets, Rect bounds)142     public void updateInternal(int rotation, Insets insets, Rect bounds) {
143         mRotation = rotation;
144         mInsets = insets;
145 
146         mPositionRect = new Rect(bounds);
147         mPositionRect.left += mInsets.left;
148         mPositionRect.top += mInsets.top;
149         mPositionRect.right -= mInsets.right;
150         mPositionRect.bottom -= mInsets.bottom;
151 
152         Resources res = mContext.getResources();
153         mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
154         mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size);
155         mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
156         mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
157 
158         mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(
159                 R.dimen.bubble_expanded_view_tablet_width);
160         mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
161         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
162         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
163         mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
164 
165         mMaxBubbles = calculateMaxBubbles();
166 
167         if (mShowingInTaskbar) {
168             adjustForTaskbar();
169         }
170     }
171 
172     /**
173      * @return the maximum number of bubbles that can fit on the screen when expanded. If the
174      * screen size / screen density is too small to support the default maximum number, then
175      * the number will be adjust to something lower to ensure everything is presented nicely.
176      */
calculateMaxBubbles()177     private int calculateMaxBubbles() {
178         // Use the shortest edge.
179         // In portrait the bubbles should align with the expanded view so subtract its padding.
180         // We always show the overflow so subtract one bubble size.
181         int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2);
182         int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height())
183                 - padding
184                 - mBubbleSize;
185         // Each of the bubbles have spacing because the overflow is at the end.
186         int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles);
187         if (howManyFit < mDefaultMaxBubbles) {
188             // Not enough space for the default.
189             return howManyFit;
190         }
191         return mDefaultMaxBubbles;
192     }
193 
194     /**
195      * Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should
196      * not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space
197      * and insets to account for the taskbar.
198      */
199     // TODO(b/171559950): When the insets are reported correctly we can remove this logic
adjustForTaskbar()200     private void adjustForTaskbar() {
201         // When bar is showing on edges... subtract that inset because we appear on top
202         if (mShowingInTaskbar && mTaskbarPosition != TASKBAR_POSITION_BOTTOM) {
203             WindowInsets metricInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
204             Insets navBarInsets = metricInsets.getInsetsIgnoringVisibility(
205                     WindowInsets.Type.navigationBars());
206             int newInsetLeft = mInsets.left;
207             int newInsetRight = mInsets.right;
208             if (mTaskbarPosition == TASKBAR_POSITION_LEFT) {
209                 mPositionRect.left -= navBarInsets.left;
210                 newInsetLeft -= navBarInsets.left;
211             } else if (mTaskbarPosition == TASKBAR_POSITION_RIGHT) {
212                 mPositionRect.right += navBarInsets.right;
213                 newInsetRight -= navBarInsets.right;
214             }
215             mInsets = Insets.of(newInsetLeft, mInsets.top, newInsetRight, mInsets.bottom);
216         }
217     }
218 
219     /**
220      * @return a rect of available screen space accounting for orientation, system bars and cutouts.
221      * Does not account for IME.
222      */
getAvailableRect()223     public Rect getAvailableRect() {
224         return mPositionRect;
225     }
226 
227     /**
228      * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its
229      * inset is not included here.
230      */
getInsets()231     public Insets getInsets() {
232         return mInsets;
233     }
234 
235     /** @return whether the device is in landscape orientation. */
isLandscape()236     public boolean isLandscape() {
237         return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
238     }
239 
240     /** @return whether the screen is considered large. */
isLargeScreen()241     public boolean isLargeScreen() {
242         return mIsLargeScreen;
243     }
244 
245     /**
246      * Indicates how bubbles appear when expanded.
247      *
248      * When false, bubbles display at the top of the screen with the expanded view
249      * below them. When true, bubbles display at the edges of the screen with the expanded view
250      * to the left or right side.
251      */
showBubblesVertically()252     public boolean showBubblesVertically() {
253         return isLandscape() || mShowingInTaskbar || mIsLargeScreen;
254     }
255 
256     /** Size of the bubble. */
getBubbleSize()257     public int getBubbleSize() {
258         return (mShowingInTaskbar && mTaskbarIconSize > 0)
259                 ? mTaskbarIconSize
260                 : mBubbleSize;
261     }
262 
263     /** The maximum number of bubbles that can be displayed comfortably on screen. */
getMaxBubbles()264     public int getMaxBubbles() {
265         return mMaxBubbles;
266     }
267 
268     /**
269      * Calculates the left & right padding for the bubble expanded view.
270      *
271      * On larger screens the width of the expanded view is restricted via this padding.
272      * On landscape the bubble overflow expanded view is also restricted via this padding.
273      */
getExpandedViewPadding(boolean onLeft, boolean isOverflow)274     public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) {
275         int leftPadding = mInsets.left + mExpandedViewPadding;
276         int rightPadding = mInsets.right + mExpandedViewPadding;
277         final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow;
278         if (showBubblesVertically()) {
279             if (!onLeft) {
280                 rightPadding += mBubbleSize - mPointerHeight;
281                 leftPadding += isLargeOrOverflow
282                         ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth)
283                         : 0;
284             } else {
285                 leftPadding += mBubbleSize - mPointerHeight;
286                 rightPadding += isLargeOrOverflow
287                         ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth)
288                         : 0;
289             }
290         }
291         // [left, top, right, bottom]
292         mPaddings[0] = leftPadding;
293         mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin;
294         mPaddings[2] = rightPadding;
295         mPaddings[3] = 0;
296         return mPaddings;
297     }
298 
299     /** Calculates the y position of the expanded view when it is expanded. */
getExpandedViewY()300     public float getExpandedViewY() {
301         final int top = getAvailableRect().top;
302         if (showBubblesVertically()) {
303             return top - mPointerWidth;
304         } else {
305             return top + mBubbleSize + mPointerMargin;
306         }
307     }
308 
309     /**
310      * Sets the stack's most recent position along the edge of the screen. This is saved when the
311      * last bubble is removed, so that the stack can be restored in its previous position.
312      */
setRestingPosition(PointF position)313     public void setRestingPosition(PointF position) {
314         if (mRestingStackPosition == null) {
315             mRestingStackPosition = new PointF(position);
316         } else {
317             mRestingStackPosition.set(position);
318         }
319     }
320 
321     /** The position the bubble stack should rest at when collapsed. */
getRestingPosition()322     public PointF getRestingPosition() {
323         if (mPinLocation != null) {
324             return mPinLocation;
325         }
326         if (mRestingStackPosition == null) {
327             return getDefaultStartPosition();
328         }
329         return mRestingStackPosition;
330     }
331 
332     /**
333      * @return the stack position to use if we don't have a saved location or if user education
334      * is being shown.
335      */
getDefaultStartPosition()336     public PointF getDefaultStartPosition() {
337         // Start on the left if we're in LTR, right otherwise.
338         final boolean startOnLeft =
339                 mContext.getResources().getConfiguration().getLayoutDirection()
340                         != View.LAYOUT_DIRECTION_RTL;
341         final float startingVerticalOffset = mContext.getResources().getDimensionPixelOffset(
342                 R.dimen.bubble_stack_starting_offset_y);
343         // TODO: placement bug here because mPositionRect doesn't handle the overhanging edge
344         return new BubbleStackView.RelativeStackPosition(
345                 startOnLeft,
346                 startingVerticalOffset / mPositionRect.height())
347                 .getAbsolutePositionInRegion(new RectF(mPositionRect));
348     }
349 
350     /**
351      * @return whether the bubble stack is pinned to the taskbar.
352      */
showingInTaskbar()353     public boolean showingInTaskbar() {
354         return mShowingInTaskbar;
355     }
356 
357     /**
358      * @return the taskbar position if set.
359      */
getTaskbarPosition()360     public int getTaskbarPosition() {
361         return mTaskbarPosition;
362     }
363 
getTaskbarSize()364     public int getTaskbarSize() {
365         return mTaskbarSize;
366     }
367 
368     /**
369      * In some situations bubbles will be pinned to a specific onscreen location. This sets the
370      * location to anchor the stack to.
371      */
setPinnedLocation(PointF point)372     public void setPinnedLocation(PointF point) {
373         mPinLocation = point;
374     }
375 }
376