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