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.dpiFromPx; 21 import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE; 22 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT; 23 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE; 24 import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE; 25 import static com.android.launcher3.testing.shared.ResourceUtils.NAV_BAR_INTERACTION_MODE_RES_NAME; 26 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT; 27 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_LANDSCAPE; 28 import static com.android.launcher3.testing.shared.ResourceUtils.STATUS_BAR_HEIGHT_PORTRAIT; 29 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride; 30 import static com.android.launcher3.util.RotationUtils.deltaRotation; 31 import static com.android.launcher3.util.RotationUtils.rotateRect; 32 import static com.android.launcher3.util.RotationUtils.rotateSize; 33 34 import android.annotation.TargetApi; 35 import android.content.Context; 36 import android.content.res.Configuration; 37 import android.content.res.Resources; 38 import android.graphics.Insets; 39 import android.graphics.Point; 40 import android.graphics.Rect; 41 import android.hardware.display.DisplayManager; 42 import android.os.Build; 43 import android.util.ArrayMap; 44 import android.util.Log; 45 import android.view.Display; 46 import android.view.DisplayCutout; 47 import android.view.Surface; 48 import android.view.WindowInsets; 49 import android.view.WindowManager; 50 import android.view.WindowMetrics; 51 52 import com.android.launcher3.R; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.testing.shared.ResourceUtils; 55 import com.android.launcher3.util.MainThreadInitializedObject; 56 import com.android.launcher3.util.NavigationMode; 57 import com.android.launcher3.util.ResourceBasedOverride; 58 import com.android.launcher3.util.WindowBounds; 59 60 /** 61 * Utility class for mocking some window manager behaviours 62 */ 63 public class WindowManagerProxy implements ResourceBasedOverride { 64 65 private static final String TAG = "WindowManagerProxy"; 66 public static final int MIN_TABLET_WIDTH = 600; 67 68 public static final MainThreadInitializedObject<WindowManagerProxy> INSTANCE = 69 forOverride(WindowManagerProxy.class, R.string.window_manager_proxy_class); 70 71 protected final boolean mTaskbarDrawnInProcess; 72 73 /** 74 * Creates a new instance of proxy, applying any overrides 75 */ newInstance(Context context)76 public static WindowManagerProxy newInstance(Context context) { 77 return Overrides.getObject(WindowManagerProxy.class, context, 78 R.string.window_manager_proxy_class); 79 } 80 WindowManagerProxy()81 public WindowManagerProxy() { 82 this(false); 83 } 84 WindowManagerProxy(boolean taskbarDrawnInProcess)85 protected WindowManagerProxy(boolean taskbarDrawnInProcess) { 86 mTaskbarDrawnInProcess = taskbarDrawnInProcess; 87 } 88 89 /** 90 * Returns a map of normalized info of internal displays to estimated window bounds 91 * for that display 92 */ estimateInternalDisplayBounds( Context displayInfoContext)93 public ArrayMap<CachedDisplayInfo, WindowBounds[]> estimateInternalDisplayBounds( 94 Context displayInfoContext) { 95 CachedDisplayInfo info = getDisplayInfo(displayInfoContext).normalize(); 96 WindowBounds[] bounds = estimateWindowBounds(displayInfoContext, info); 97 ArrayMap<CachedDisplayInfo, WindowBounds[]> result = new ArrayMap<>(); 98 result.put(info, bounds); 99 return result; 100 } 101 102 /** 103 * Returns the real bounds for the provided display after applying any insets normalization 104 */ 105 @TargetApi(Build.VERSION_CODES.R) getRealBounds(Context displayInfoContext, CachedDisplayInfo info)106 public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) { 107 if (!Utilities.ATLEAST_R) { 108 Point smallestSize = new Point(); 109 Point largestSize = new Point(); 110 getDisplay(displayInfoContext).getCurrentSizeRange(smallestSize, largestSize); 111 112 if (info.size.y > info.size.x) { 113 // Portrait 114 return new WindowBounds(info.size.x, info.size.y, smallestSize.x, largestSize.y, 115 info.rotation); 116 } else { 117 // Landscape 118 return new WindowBounds(info.size.x, info.size.y, largestSize.x, smallestSize.y, 119 info.rotation); 120 } 121 } 122 123 WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) 124 .getMaximumWindowMetrics(); 125 Rect insets = new Rect(); 126 normalizeWindowInsets(displayInfoContext, windowMetrics.getWindowInsets(), insets); 127 return new WindowBounds(windowMetrics.getBounds(), insets, info.rotation); 128 } 129 130 /** 131 * Returns an updated insets, accounting for various Launcher UI specific overrides like taskbar 132 */ 133 @TargetApi(Build.VERSION_CODES.R) normalizeWindowInsets(Context context, WindowInsets oldInsets, Rect outInsets)134 public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets, 135 Rect outInsets) { 136 if (!Utilities.ATLEAST_R || !mTaskbarDrawnInProcess) { 137 outInsets.set(oldInsets.getSystemWindowInsetLeft(), oldInsets.getSystemWindowInsetTop(), 138 oldInsets.getSystemWindowInsetRight(), oldInsets.getSystemWindowInsetBottom()); 139 return oldInsets; 140 } 141 142 WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(oldInsets); 143 Insets navInsets = oldInsets.getInsets(WindowInsets.Type.navigationBars()); 144 145 Resources systemRes = context.getResources(); 146 Configuration config = systemRes.getConfiguration(); 147 148 boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH; 149 boolean isGesture = isGestureNav(context); 150 boolean isPortrait = config.screenHeightDp > config.screenWidthDp; 151 152 int bottomNav = isTablet 153 ? 0 154 : (isPortrait 155 ? getDimenByName(systemRes, NAVBAR_HEIGHT) 156 : (isGesture 157 ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) 158 : 0)); 159 Insets newNavInsets = Insets.of(navInsets.left, navInsets.top, navInsets.right, bottomNav); 160 insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets); 161 insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets); 162 163 Insets statusBarInsets = oldInsets.getInsets(WindowInsets.Type.statusBars()); 164 165 Insets newStatusBarInsets = Insets.of( 166 statusBarInsets.left, 167 getStatusBarHeight(context, isPortrait, statusBarInsets.top), 168 statusBarInsets.right, 169 statusBarInsets.bottom); 170 insetsBuilder.setInsets(WindowInsets.Type.statusBars(), newStatusBarInsets); 171 insetsBuilder.setInsetsIgnoringVisibility( 172 WindowInsets.Type.statusBars(), newStatusBarInsets); 173 174 // Override the tappable insets to be 0 on the bottom for gesture nav (otherwise taskbar 175 // would count towards it). This is used for the bottom protection in All Apps for example. 176 if (isGesture) { 177 Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement()); 178 Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top, 179 oldTappableInsets.right, 0); 180 insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets); 181 } 182 183 WindowInsets result = insetsBuilder.build(); 184 Insets systemWindowInsets = result.getInsetsIgnoringVisibility( 185 WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); 186 outInsets.set(systemWindowInsets.left, systemWindowInsets.top, systemWindowInsets.right, 187 systemWindowInsets.bottom); 188 return result; 189 } 190 getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset)191 protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) { 192 Resources systemRes = context.getResources(); 193 int statusBarHeight = getDimenByName(systemRes, 194 isPortrait ? STATUS_BAR_HEIGHT_PORTRAIT : STATUS_BAR_HEIGHT_LANDSCAPE, 195 STATUS_BAR_HEIGHT); 196 197 return Math.max(statusBarInset, statusBarHeight); 198 } 199 200 /** 201 * Returns a list of possible WindowBounds for the display keyed on the 4 surface rotations 202 */ estimateWindowBounds(Context context, CachedDisplayInfo displayInfo)203 protected WindowBounds[] estimateWindowBounds(Context context, CachedDisplayInfo displayInfo) { 204 int densityDpi = context.getResources().getConfiguration().densityDpi; 205 int rotation = displayInfo.rotation; 206 Rect safeCutout = displayInfo.cutout; 207 208 int minSize = Math.min(displayInfo.size.x, displayInfo.size.y); 209 int swDp = (int) dpiFromPx(minSize, densityDpi); 210 211 Resources systemRes; 212 { 213 Configuration conf = new Configuration(); 214 conf.smallestScreenWidthDp = swDp; 215 systemRes = context.createConfigurationContext(conf).getResources(); 216 } 217 218 boolean isTablet = swDp >= MIN_TABLET_WIDTH; 219 boolean isTabletOrGesture = isTablet 220 || (Utilities.ATLEAST_R && isGestureNav(context)); 221 222 // Use the status bar height resources because current system API to get the status bar 223 // height doesn't allow to do this for an arbitrary display, it returns value only 224 // for the current active display (see com.android.internal.policy.StatusBarUtils) 225 int statusBarHeightPortrait = getDimenByName(systemRes, 226 STATUS_BAR_HEIGHT_PORTRAIT, STATUS_BAR_HEIGHT); 227 int statusBarHeightLandscape = getDimenByName(systemRes, 228 STATUS_BAR_HEIGHT_LANDSCAPE, STATUS_BAR_HEIGHT); 229 230 int navBarHeightPortrait, navBarHeightLandscape, navbarWidthLandscape; 231 232 navBarHeightPortrait = isTablet 233 ? (mTaskbarDrawnInProcess 234 ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size)) 235 : getDimenByName(systemRes, NAVBAR_HEIGHT); 236 237 navBarHeightLandscape = isTablet 238 ? (mTaskbarDrawnInProcess 239 ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size)) 240 : (isTabletOrGesture 241 ? getDimenByName(systemRes, NAVBAR_HEIGHT_LANDSCAPE) : 0); 242 navbarWidthLandscape = isTabletOrGesture 243 ? 0 244 : getDimenByName(systemRes, NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE); 245 246 WindowBounds[] result = new WindowBounds[4]; 247 Point tempSize = new Point(); 248 for (int i = 0; i < 4; i++) { 249 int rotationChange = deltaRotation(rotation, i); 250 tempSize.set(displayInfo.size.x, displayInfo.size.y); 251 rotateSize(tempSize, rotationChange); 252 Rect bounds = new Rect(0, 0, tempSize.x, tempSize.y); 253 254 int navBarHeight, navbarWidth, statusBarHeight; 255 if (tempSize.y > tempSize.x) { 256 navBarHeight = navBarHeightPortrait; 257 navbarWidth = 0; 258 statusBarHeight = statusBarHeightPortrait; 259 } else { 260 navBarHeight = navBarHeightLandscape; 261 navbarWidth = navbarWidthLandscape; 262 statusBarHeight = statusBarHeightLandscape; 263 } 264 265 Rect insets = new Rect(safeCutout); 266 rotateRect(insets, rotationChange); 267 insets.top = Math.max(insets.top, statusBarHeight); 268 insets.bottom = Math.max(insets.bottom, navBarHeight); 269 270 if (i == Surface.ROTATION_270 || i == Surface.ROTATION_180) { 271 // On reverse landscape (and in rare-case when the natural orientation of the 272 // device is landscape), navigation bar is on the right. 273 insets.left = Math.max(insets.left, navbarWidth); 274 } else { 275 insets.right = Math.max(insets.right, navbarWidth); 276 } 277 result[i] = new WindowBounds(bounds, insets, i); 278 } 279 return result; 280 } 281 282 /** 283 * Wrapper around the utility method for easier emulation 284 */ getDimenByName(Resources res, String resName)285 protected int getDimenByName(Resources res, String resName) { 286 return ResourceUtils.getDimenByName(resName, res, 0); 287 } 288 289 /** 290 * Wrapper around the utility method for easier emulation 291 */ getDimenByName(Resources res, String resName, String fallback)292 protected int getDimenByName(Resources res, String resName, String fallback) { 293 int dimen = ResourceUtils.getDimenByName(resName, res, -1); 294 return dimen > -1 ? dimen : getDimenByName(res, fallback); 295 } 296 isGestureNav(Context context)297 protected boolean isGestureNav(Context context) { 298 return ResourceUtils.getIntegerByName("config_navBarInteractionMode", 299 context.getResources(), INVALID_RESOURCE_HANDLE) == 2; 300 } 301 302 /** 303 * Returns a CachedDisplayInfo initialized for the current display 304 */ 305 @TargetApi(Build.VERSION_CODES.S) getDisplayInfo(Context displayInfoContext)306 public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) { 307 int rotation = getRotation(displayInfoContext); 308 if (Utilities.ATLEAST_S) { 309 WindowMetrics windowMetrics = displayInfoContext.getSystemService(WindowManager.class) 310 .getMaximumWindowMetrics(); 311 return getDisplayInfo(windowMetrics, rotation); 312 } else { 313 Point size = new Point(); 314 Display display = getDisplay(displayInfoContext); 315 display.getRealSize(size); 316 Rect cutoutRect = new Rect(); 317 return new CachedDisplayInfo(size, rotation, cutoutRect); 318 } 319 } 320 321 /** 322 * Returns a CachedDisplayInfo initialized for the current display 323 */ 324 @TargetApi(Build.VERSION_CODES.S) getDisplayInfo(WindowMetrics windowMetrics, int rotation)325 protected CachedDisplayInfo getDisplayInfo(WindowMetrics windowMetrics, int rotation) { 326 Point size = new Point(windowMetrics.getBounds().right, windowMetrics.getBounds().bottom); 327 Rect cutoutRect = new Rect(); 328 DisplayCutout cutout = windowMetrics.getWindowInsets().getDisplayCutout(); 329 if (cutout != null) { 330 cutoutRect.set(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), 331 cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); 332 } 333 return new CachedDisplayInfo(size, rotation, cutoutRect); 334 } 335 336 /** 337 * Returns rotation of the display associated with the context, or rotation of DEFAULT_DISPLAY 338 * if the context isn't associated with a display. 339 */ getRotation(Context displayInfoContext)340 public int getRotation(Context displayInfoContext) { 341 return getDisplay(displayInfoContext).getRotation(); 342 } 343 344 /** 345 * 346 * Returns the display associated with the context, or DEFAULT_DISPLAY if the context isn't 347 * associated with a display. 348 */ getDisplay(Context displayInfoContext)349 protected Display getDisplay(Context displayInfoContext) { 350 if (Utilities.ATLEAST_R) { 351 try { 352 return displayInfoContext.getDisplay(); 353 } catch (UnsupportedOperationException e) { 354 // Ignore 355 } 356 } 357 return displayInfoContext.getSystemService(DisplayManager.class).getDisplay( 358 DEFAULT_DISPLAY); 359 } 360 361 /** 362 * Returns the current navigation mode from resource. 363 */ getNavigationMode(Context context)364 public NavigationMode getNavigationMode(Context context) { 365 int modeInt = ResourceUtils.getIntegerByName(NAV_BAR_INTERACTION_MODE_RES_NAME, 366 context.getResources(), INVALID_RESOURCE_HANDLE); 367 368 if (modeInt == INVALID_RESOURCE_HANDLE) { 369 Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); 370 } else { 371 for (NavigationMode m : NavigationMode.values()) { 372 if (m.resValue == modeInt) { 373 return m; 374 } 375 } 376 } 377 return Utilities.ATLEAST_S ? NavigationMode.NO_BUTTON : 378 NavigationMode.THREE_BUTTONS; 379 } 380 } 381