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