1 /* 2 * Copyright (C) 2022 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 package com.android.launcher3.util.window; 17 18 import static android.view.Display.DEFAULT_DISPLAY; 19 20 import static com.android.launcher3.Utilities.dpToPx; 21 import static com.android.launcher3.Utilities.dpiFromPx; 22 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE; 23 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT; 24 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE; 25 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE; 26 import static com.android.launcher3.testing.shared.ResourceUtils.NAV_BAR_INTERACTION_MODE_RES_NAME; 27 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT; 28 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_LANDSCAPE; 29 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_PORTRAIT; 30 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; 31 import static com.android.launcher3.util.RotationUtils.deltaRotation; 32 import static com.android.launcher3.util.RotationUtils.rotateRect; 33 import static com.android.launcher3.util.RotationUtils.rotateSize; 34 35 import android.annotation.TargetApi; 36 import android.content.Context; 37 import android.content.res.Configuration; 38 import android.content.res.Resources; 39 import android.graphics.Insets; 40 import android.graphics.Point; 41 import android.graphics.Rect; 42 import android.hardware.display.DisplayManager; 43 import android.os.Build; 44 import android.util.ArrayMap; 45 import android.util.Log; 46 import android.view.Display; 47 import android.view.DisplayCutout; 48 import android.view.Surface; 49 import android.view.WindowInsets; 50 import android.view.WindowManager; 51 import android.view.WindowMetrics; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.VisibleForTesting; 55 56 import com.android.launcher3.R; 57 import com.android.launcher3.Utilities; 58 import com.android.launcher3.testing.shared.ResourceUtils; 59 import com.android.launcher3.util.MainThreadInitializedObject; 60 import com.android.launcher3.util.NavigationMode; 61 import com.android.launcher3.util.ResourceBasedOverride; 62 import com.android.launcher3.util.SafeCloseable; 63 import com.android.launcher3.util.WindowBounds; 64 65 import java.util.ArrayList; 66 import java.util.List; 67 68 /** 69 * Utility class for mocking some window manager behaviours 70 */ 71 public class WindowManagerProxy implements ResourceBasedOverride, SafeCloseable { 72 73 private static final String TAG = "WindowManagerProxy"; 74 public static final int MIN_TABLET_WIDTH = 600; 75 76 public static final MainThreadInitializedObject<WindowManagerProxy> INSTANCE = 77 forOverride(WindowManagerProxy.class, R.string.window_manager_proxy_class); 78 79 protected final boolean mTaskbarDrawnInProcess; 80 81 /** 82 * Creates a new instance of proxy, applying any overrides 83 */ newInstance(Context context)84 public static WindowManagerProxy newInstance(Context context) { 85 return Overrides.getObject(WindowManagerProxy.class, context, 86 R.string.window_manager_proxy_class); 87 } 88 WindowManagerProxy()89 public WindowManagerProxy() { 90 this(false); 91 } 92 WindowManagerProxy(boolean taskbarDrawnInProcess)93 protected WindowManagerProxy(boolean taskbarDrawnInProcess) { 94 mTaskbarDrawnInProcess = taskbarDrawnInProcess; 95 } 96 97 /** 98 * Returns true if taskbar is drawn in process 99 */ isTaskbarDrawnInProcess()100 public boolean isTaskbarDrawnInProcess() { 101 return mTaskbarDrawnInProcess; 102 } 103 104 /** 105 * Returns a map of normalized info of internal displays to estimated window bounds 106 * for that display 107 */ estimateInternalDisplayBounds( Context displayInfoContext)108 public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds( 109 Context displayInfoContext) { 110 CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize(this); 111 List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info); 112 ArrayMap<CachedDisplayInfo, List<WindowBounds>> result = new ArrayMap<>(); 113 result.put(info, bounds); 114 return result; 115 } 116 117 /** 118 * Returns if we are in desktop mode or not. 119 */ isInDesktopMode()120 public boolean isInDesktopMode() { 121 return false; 122 } 123 124 /** 125 * Returns the real bounds for the provided display after applying any insets normalization 126 */ getRealBounds(Context displayInfoContext, CachedDisplayInfo info)127 public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) { 128 WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) 129 .getMaximumWindowMetrics(); 130 Rect insets = new Rect(); 131 normalizeWindowInsets(displayInfoContext, windowMetrics.getWindowInsets(), insets); 132 return new WindowBounds(windowMetrics.getBounds(), insets, info.rotation); 133 } 134 135 /** 136 * Returns an updated insets, accounting for various Launcher UI specific overrides like taskbar 137 */ normalizeWindowInsets(Context context, WindowInsets oldInsets, Rect outInsets)138 public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets, 139 Rect outInsets) { 140 if (!mTaskbarDrawnInProcess) { 141 outInsets.set(oldInsets.getSystemWindowInsetLeft(), oldInsets.getSystemWindowInsetTop(), 142 oldInsets.getSystemWindowInsetRight(), oldInsets.getSystemWindowInsetBottom()); 143 return oldInsets; 144 } 145 146 WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(oldInsets); 147 Insets navInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); 148 149 Resources systemRes = context.getResources(); 150 Configuration config = systemRes.getConfiguration(); 151 152 boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH; 153 boolean isGesture = isGestureNav(context); 154 boolean isPortrait = config.screenHeightDp > config.screenWidthDp; 155 156 int bottomNav = isLargeScreen 157 ? 0 158 : (isPortrait 159 ? getDimenByName(systemRes, NAVBAR_HEIGHT) 160 : (isGesture 161 ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) 162 : 0)); 163 int leftNav = navInsets.left; 164 int rightNav = navInsets.right; 165 if (!isLargeScreen && !isGesture && !isPortrait) { 166 // In 3-button landscape/seascape, Launcher should always have nav insets regardless if 167 // it's initiated from fullscreen apps. 168 int navBarWidth = getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE); 169 switch (getRotation(context)) { 170 case Surface.ROTATION_90 -> rightNav = navBarWidth; 171 case Surface.ROTATION_270 -> leftNav = navBarWidth; 172 } 173 } 174 Insets newNavInsets = Insets.of(leftNav, navInsets.top, rightNav, bottomNav); 175 insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); 176 insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets); 177 178 Insets statusBarInsets = oldInsets.getInsets(WindowInsets.Type.statusBars()); 179 180 Insets newStatusBarInsets = Insets.of( 181 statusBarInsets.left, 182 getStatusBarHeight(context, isPortrait, statusBarInsets.top), 183 statusBarInsets.right, 184 statusBarInsets.bottom); 185 insetsBuilder.setInsets(WindowInsets.Type.statusBars(), newStatusBarInsets); 186 insetsBuilder.setInsetsIgnoringVisibility( 187 WindowInsets.Type.statusBars(), newStatusBarInsets); 188 189 // Override the tappable insets to be 0 on the bottom for gesture nav (otherwise taskbar 190 // would count towards it). This is used for the bottom protection in All Apps for example. 191 if (isGesture) { 192 Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); 193 Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top, 194 oldTappableInsets.right, 0); 195 insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); 196 } 197 198 applyDisplayCutoutBottomInsetOverrideOnLargeScreen( 199 context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder); 200 201 WindowInsets result = insetsBuilder.build(); 202 Insets systemWindowInsets = result.getInsetsIgnoringVisibility( 203 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); 204 outInsets.set(systemWindowInsets.left, systemWindowInsets.top, systemWindowInsets.right, 205 systemWindowInsets.bottom); 206 return result; 207 } 208 209 /** 210 * For large screen, when display cutout is at bottom left/right corner of screen, override 211 * display cutout's bottom inset to 0, because launcher allows drawing content over that area. 212 */ applyDisplayCutoutBottomInsetOverrideOnLargeScreen( @onNull Context context, boolean isLargeScreen, int screenWidthPx, @NonNull WindowInsets windowInsets, @NonNull WindowInsets.Builder insetsBuilder)213 private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen( 214 @NonNull Context context, 215 boolean isLargeScreen, 216 int screenWidthPx, 217 @NonNull WindowInsets windowInsets, 218 @NonNull WindowInsets.Builder insetsBuilder) { 219 if (!isLargeScreen || !Utilities.ATLEAST_S) { 220 return; 221 } 222 223 final DisplayCutout displayCutout = windowInsets.getDisplayCutout(); 224 if (displayCutout == null) { 225 return; 226 } 227 228 if (!areBottomDisplayCutoutsSmallAndAtCorners( 229 displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) { 230 return; 231 } 232 233 Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout()); 234 Insets newDisplayCutoutInset = Insets.of( 235 oldDisplayCutoutInset.left, 236 oldDisplayCutoutInset.top, 237 oldDisplayCutoutInset.right, 238 0); 239 insetsBuilder.setInsetsIgnoringVisibility( 240 WindowInsets.Type.displayCutout(), newDisplayCutoutInset); 241 } 242 243 /** 244 * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)} 245 */ areBottomDisplayCutoutsSmallAndAtCorners( @onNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res)246 private static boolean areBottomDisplayCutoutsSmallAndAtCorners( 247 @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) { 248 return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx, 249 res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout)); 250 } 251 252 /** 253 * Return true if bottom display cutouts are at bottom left/right corners, AND has width or 254 * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx 255 * passed to this method should be in the SAME screen rotation. 256 * 257 * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation 258 * @param screenWidthPx screen width in px based on current screen rotation 259 * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout. 260 */ 261 @VisibleForTesting areBottomDisplayCutoutsSmallAndAtCorners( @onNull Rect cutoutRectBottom, int screenWidthPx, int maxWidthAndHeightOfSmallCutoutPx)262 static boolean areBottomDisplayCutoutsSmallAndAtCorners( 263 @NonNull Rect cutoutRectBottom, int screenWidthPx, 264 int maxWidthAndHeightOfSmallCutoutPx) { 265 // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore 266 // it by returning false. 267 if (cutoutRectBottom.isEmpty()) { 268 return false; 269 } 270 return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx) 271 || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx); 272 } 273 getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset)274 protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) { 275 Resources systemRes = context.getResources(); 276 int statusBarHeight = getDimenByName(systemRes, 277 isPortrait ? STATUS_BAR_HEIGHT_PORTRAIT : STATUS_BAR_HEIGHT_LANDSCAPE, 278 STATUS_BAR_HEIGHT); 279 280 return Math.max(statusBarInset, statusBarHeight); 281 } 282 283 /** 284 * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations 285 */ estimateWindowBounds(Context context, final CachedDisplayInfo displayInfo)286 protected List<WindowBounds> estimateWindowBounds(Context context, 287 final CachedDisplayInfo displayInfo) { 288 int densityDpi = context.getResources().getConfiguration().densityDpi; 289 final int rotation = displayInfo.rotation; 290 291 int minSize = Math.min(displayInfo.size.x, displayInfo.size.y); 292 int swDp = (int) dpiFromPx(minSize, densityDpi); 293 294 Resources systemRes; 295 { 296 Configuration conf = new Configuration(); 297 conf.smallestScreenWidthDp = swDp; 298 systemRes = context.createConfigurationContext(conf).getResources(); 299 } 300 301 boolean isTablet = swDp >= MIN_TABLET_WIDTH; 302 boolean isTabletOrGesture = isTablet || isGestureNav(context); 303 304 // Use the status bar height resources because current system API to get the status bar 305 // height doesn't allow to do this for an arbitrary display, it returns value only 306 // for the current active display (see com.android.internal.policy.StatusBarUtils) 307 int statusBarHeightPortrait = getDimenByName(systemRes, 308 STATUS_BAR_HEIGHT_PORTRAIT, STATUS_BAR_HEIGHT); 309 int statusBarHeightLandscape = getDimenByName(systemRes, 310 STATUS_BAR_HEIGHT_LANDSCAPE, STATUS_BAR_HEIGHT); 311 312 int navBarHeightPortrait, navBarHeightLandscape, navbarWidthLandscape; 313 314 navBarHeightPortrait = isTablet 315 ? (mTaskbarDrawnInProcess 316 ? 0 : context.getResources().getDimensionPixelSize(R.dimen.taskbar_size)) 317 : getDimenByName(systemRes, NAVBAR_HEIGHT); 318 319 navBarHeightLandscape = isTablet 320 ? (mTaskbarDrawnInProcess 321 ? 0 : context.getResources().getDimensionPixelSize(R.dimen.taskbar_size)) 322 : (isTabletOrGesture 323 ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) : 0); 324 navbarWidthLandscape = isTabletOrGesture 325 ? 0 326 : getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE); 327 328 List<WindowBounds> result = new ArrayList<>(4); 329 Point tempSize = new Point(); 330 for (int i = 0; i < 4; i++) { 331 int rotationChange = deltaRotation(rotation, i); 332 tempSize.set(displayInfo.size.x, displayInfo.size.y); 333 rotateSize(tempSize, rotationChange); 334 Rect bounds = new Rect(0, 0, tempSize.x, tempSize.y); 335 336 int navBarHeight, navbarWidth, statusBarHeight; 337 if (tempSize.y > tempSize.x) { 338 navBarHeight = navBarHeightPortrait; 339 navbarWidth = 0; 340 statusBarHeight = statusBarHeightPortrait; 341 } else { 342 navBarHeight = navBarHeightLandscape; 343 navbarWidth = navbarWidthLandscape; 344 statusBarHeight = statusBarHeightLandscape; 345 } 346 347 DisplayCutout rotatedCutout = rotateCutout( 348 displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i); 349 Rect insets = getSafeInsets(rotatedCutout); 350 if (areBottomDisplayCutoutsSmallAndAtCorners( 351 rotatedCutout.getBoundingRectBottom(), 352 bounds.width(), 353 context.getResources())) { 354 insets.bottom = 0; 355 } 356 insets.top = Math.max(insets.top, statusBarHeight); 357 insets.bottom = Math.max(insets.bottom, navBarHeight); 358 359 if (i == Surface.ROTATION_270 || i == Surface.ROTATION_180) { 360 // On reverse landscape (and in rare-case when the natural orientation of the 361 // device is landscape), navigation bar is on the right. 362 insets.left = Math.max(insets.left, navbarWidth); 363 } else { 364 insets.right = Math.max(insets.right, navbarWidth); 365 } 366 result.add(new WindowBounds(bounds, insets, i)); 367 } 368 return result; 369 } 370 371 /** 372 * Wrapper around the utility method for easier emulation 373 */ getDimenByName(Resources res, String resName)374 protected int getDimenByName(Resources res, String resName) { 375 return ResourceUtils.getDimenByName(resName, res, 0); 376 } 377 378 /** 379 * Wrapper around the utility method for easier emulation 380 */ getDimenByName(Resources res, String resName, String fallback)381 protected int getDimenByName(Resources res, String resName, String fallback) { 382 int dimen = ResourceUtils.getDimenByName(resName, res, -1); 383 return dimen > -1 ? dimen : getDimenByName(res, fallback); 384 } 385 isGestureNav(Context context)386 protected boolean isGestureNav(Context context) { 387 return ResourceUtils.getIntegerByName("config_navBarInteractionMode", 388 context.getResources(), INVALID_RESOURCE_HANDLE) == 2; 389 } 390 391 /** 392 * Returns a CachedDisplayInfo initialized for the current display 393 */ 394 @TargetApi(Build.VERSION_CODES.S) getDisplayInfo(Context displayInfoContext)395 public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) { 396 int rotation = getRotation(displayInfoContext); 397 if (Utilities.ATLEAST_S) { 398 WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) 399 .getMaximumWindowMetrics(); 400 return getDisplayInfo(windowMetrics, rotation); 401 } else { 402 Point size = new Point(); 403 Display display = getDisplay(displayInfoContext); 404 display.getRealSize(size); 405 return new CachedDisplayInfo(size, rotation); 406 } 407 } 408 409 /** 410 * Returns a CachedDisplayInfo initialized for the current display 411 */ 412 @TargetApi(Build.VERSION_CODES.S) getDisplayInfo(WindowMetrics windowMetrics, int rotation)413 protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) { 414 Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom); 415 return new CachedDisplayInfo(size, rotation, 416 windowMetrics.getWindowInsets().getDisplayCutout()); 417 } 418 419 /** 420 * Returns bounds of the display associated with the context, or bounds of DEFAULT_DISPLAY 421 * if the context isn't associated with a display. 422 */ getCurrentBounds(Context displayInfoContext)423 public Rect getCurrentBounds(Context displayInfoContext) { 424 Resources res = displayInfoContext.getResources(); 425 Configuration config = res.getConfiguration(); 426 427 float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density; 428 float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density; 429 430 return new Rect(0, 0, (int) screenWidth, (int) screenHeight); 431 } 432 433 /** 434 * Returns rotation of the display associated with the context, or rotation of DEFAULT_DISPLAY 435 * if the context isn't associated with a display. 436 */ getRotation(Context displayInfoContext)437 public int getRotation(Context displayInfoContext) { 438 return getDisplay(displayInfoContext).getRotation(); 439 } 440 441 /** 442 * Returns the display associated with the context, or DEFAULT_DISPLAY if the context isn't 443 * associated with a display. 444 */ getDisplay(Context displayInfoContext)445 protected Display getDisplay(Context displayInfoContext) { 446 try { 447 return displayInfoContext.getDisplay(); 448 } catch (UnsupportedOperationException e) { 449 // Ignore 450 } 451 return displayInfoContext.getSystemService(DisplayManager.class).getDisplay( 452 DEFAULT_DISPLAY); 453 } 454 455 /** 456 * Returns a DisplayCutout which represents a rotated version of the original 457 */ rotateCutout(DisplayCutout original, int startWidth, int startHeight, int fromRotation, int toRotation)458 protected DisplayCutout rotateCutout(DisplayCutout original, int startWidth, int startHeight, 459 int fromRotation, int toRotation) { 460 Rect safeCutout = getSafeInsets(original); 461 rotateRect(safeCutout, deltaRotation(fromRotation, toRotation)); 462 return new DisplayCutout(Insets.of(safeCutout), null, null, null, null); 463 } 464 465 /** 466 * Returns the current navigation mode from resource. 467 */ getNavigationMode(Context context)468 public NavigationMode getNavigationMode(Context context) { 469 int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME, 470 context.getResources(), INVALID_RESOURCE_HANDLE); 471 472 if (modeInt == INVALID_RESOURCE_HANDLE) { 473 Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); 474 } else { 475 for (NavigationMode m : NavigationMode.values()) { 476 if (m.resValue == modeInt) { 477 return m; 478 } 479 } 480 } 481 return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON : 482 NavigationMode.THREE_BUTTONS; 483 } 484 485 @Override close()486 public void close() { } 487 488 /** 489 * @see DisplayCutout#getSafeInsets 490 */ getSafeInsets(DisplayCutout cutout)491 public static Rect getSafeInsets(DisplayCutout cutout) { 492 return new Rect(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), 493 cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); 494 } 495 } 496