1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; 4 import static android.os.Build.VERSION_CODES.M; 5 import static android.os.Build.VERSION_CODES.N; 6 import static android.os.Build.VERSION_CODES.N_MR1; 7 import static android.os.Build.VERSION_CODES.O; 8 import static android.os.Build.VERSION_CODES.P; 9 import static android.os.Build.VERSION_CODES.Q; 10 import static android.os.Build.VERSION_CODES.R; 11 import static android.os.Build.VERSION_CODES.S; 12 import static android.os.Build.VERSION_CODES.S_V2; 13 import static android.os.Build.VERSION_CODES.TIRAMISU; 14 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 15 import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; 16 import static android.view.WindowInsets.Type.navigationBars; 17 import static android.view.WindowInsets.Type.statusBars; 18 import static android.view.WindowInsets.Type.systemBars; 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 import static com.google.common.base.Preconditions.checkState; 22 import static java.lang.Math.max; 23 import static java.lang.Math.round; 24 import static java.util.Arrays.stream; 25 import static org.robolectric.shadows.ShadowView.useRealGraphics; 26 import static org.robolectric.shadows.SystemUi.systemUiForDisplay; 27 import static org.robolectric.util.ReflectionHelpers.callConstructor; 28 import static org.robolectric.util.ReflectionHelpers.callInstanceMethod; 29 import static org.robolectric.util.reflector.Reflector.reflector; 30 31 import android.annotation.FloatRange; 32 import android.annotation.Nullable; 33 import android.app.Instrumentation; 34 import android.content.ClipData; 35 import android.content.Context; 36 import android.content.res.Configuration; 37 import android.graphics.Insets; 38 import android.graphics.Point; 39 import android.graphics.Rect; 40 import android.hardware.display.DisplayManagerGlobal; 41 import android.os.Binder; 42 import android.os.IBinder; 43 import android.os.RemoteException; 44 import android.os.ServiceManager; 45 import android.os.SystemClock; 46 import android.util.Log; 47 import android.util.MergedConfiguration; 48 import android.view.DisplayCutout; 49 import android.view.DisplayInfo; 50 import android.view.Gravity; 51 import android.view.IWindow; 52 import android.view.IWindowManager; 53 import android.view.IWindowSession; 54 import android.view.InsetsSource; 55 import android.view.InsetsSourceControl; 56 import android.view.InsetsState; 57 import android.view.MotionEvent; 58 import android.view.RemoteAnimationTarget; 59 import android.view.SurfaceControl; 60 import android.view.View; 61 import android.view.ViewConfiguration; 62 import android.view.WindowManager; 63 import android.view.WindowManager.LayoutParams; 64 import android.view.WindowManagerGlobal; 65 import android.view.WindowRelayoutResult; 66 import android.window.ActivityWindowInfo; 67 import android.window.BackEvent; 68 import android.window.BackMotionEvent; 69 import android.window.ClientWindowFrames; 70 import android.window.OnBackInvokedCallbackInfo; 71 import com.google.errorprone.annotations.CanIgnoreReturnValue; 72 import java.io.Closeable; 73 import java.lang.reflect.Array; 74 import java.lang.reflect.Modifier; 75 import java.lang.reflect.Proxy; 76 import java.util.ArrayList; 77 import java.util.Arrays; 78 import java.util.LinkedHashMap; 79 import java.util.List; 80 import java.util.Map.Entry; 81 import org.robolectric.RuntimeEnvironment; 82 import org.robolectric.annotation.ClassName; 83 import org.robolectric.annotation.Implementation; 84 import org.robolectric.annotation.Implements; 85 import org.robolectric.annotation.Resetter; 86 import org.robolectric.shadow.api.Shadow; 87 import org.robolectric.shadows.ShadowInsetsState.InsetsStateReflector; 88 import org.robolectric.util.ReflectionHelpers; 89 import org.robolectric.util.ReflectionHelpers.ClassParameter; 90 import org.robolectric.util.reflector.Accessor; 91 import org.robolectric.util.reflector.Constructor; 92 import org.robolectric.util.reflector.ForType; 93 import org.robolectric.util.reflector.Static; 94 95 /** Shadow for {@link WindowManagerGlobal}. */ 96 @SuppressWarnings("unused") // Unused params are implementations of Android SDK methods. 97 @Implements(value = WindowManagerGlobal.class, isInAndroidSdk = false) 98 public class ShadowWindowManagerGlobal { 99 private static WindowSessionDelegate windowSessionDelegate = new WindowSessionDelegate(); 100 private static IWindowSession windowSession; 101 102 @Resetter reset()103 public static void reset() { 104 reflector(WindowManagerGlobalReflector.class).setDefaultWindowManager(null); 105 windowSessionDelegate = new WindowSessionDelegate(); 106 windowSession = null; 107 } 108 getInTouchMode()109 public static boolean getInTouchMode() { 110 return windowSessionDelegate.getInTouchMode(); 111 } 112 113 /** 114 * Sets whether the window manager is in touch mode. Use {@link 115 * Instrumentation#setInTouchMode(boolean)} to modify this from a test. 116 */ setInTouchMode(boolean inTouchMode)117 static void setInTouchMode(boolean inTouchMode) { 118 windowSessionDelegate.setInTouchMode(inTouchMode); 119 } 120 notifyResize(IWindow window)121 static void notifyResize(IWindow window) { 122 windowSessionDelegate.sendResize(window); 123 } 124 125 /** 126 * Returns the last {@link ClipData} passed to a drag initiated from a call to {@link 127 * View#startDrag} or {@link View#startDragAndDrop}, or null if there isn't one. 128 */ 129 @Nullable getLastDragClipData()130 public static ClipData getLastDragClipData() { 131 return windowSessionDelegate.lastDragClipData; 132 } 133 134 /** Clears the data returned by {@link #getLastDragClipData()}. */ clearLastDragClipData()135 public static void clearLastDragClipData() { 136 windowSessionDelegate.lastDragClipData = null; 137 } 138 139 /** 140 * Ongoing predictive back gesture. 141 * 142 * <p>Start a predictive back gesture by calling {@link 143 * ShadowWindowManagerGlobal#startPredictiveBackGesture}. One or more drag progress events can be 144 * dispatched by calling {@link #moveBy}. The gesture must be ended by either calling {@link 145 * #cancel()} or {@link #close()}, if {@link #cancel()} is called a subsequent call to {@link 146 * close()} will do nothing to allow using the gesture in a try with resources statement: 147 * 148 * <pre> 149 * try (PredictiveBackGesture backGesture = 150 * ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT)) { 151 * backGesture.moveBy(10, 10); 152 * } 153 * </pre> 154 */ 155 public static final class PredictiveBackGesture implements Closeable { 156 @BackEvent.SwipeEdge private final int edge; 157 private final int displayWidth; 158 private final float startTouchX; 159 private final float progressThreshold; 160 private float touchX; 161 private float touchY; 162 private boolean isCancelled; 163 private boolean isFinished; 164 PredictiveBackGesture( @ackEvent.SwipeEdge int edge, int displayWidth, float touchX, float touchY)165 private PredictiveBackGesture( 166 @BackEvent.SwipeEdge int edge, int displayWidth, float touchX, float touchY) { 167 this.edge = edge; 168 this.displayWidth = displayWidth; 169 this.progressThreshold = 170 ViewConfiguration.get(RuntimeEnvironment.getApplication()).getScaledTouchSlop(); 171 this.startTouchX = touchX; 172 this.touchX = touchX; 173 this.touchY = touchY; 174 } 175 176 /** Dispatches drag progress for a predictive back gesture. */ moveBy(float dx, float dy)177 public void moveBy(float dx, float dy) { 178 checkState(!isCancelled && !isFinished); 179 try { 180 touchX += dx; 181 touchY += dy; 182 ShadowWindowManagerGlobal.windowSessionDelegate 183 .onBackInvokedCallbackInfo 184 .getCallback() 185 .onBackProgressed( 186 BackMotionEvents.newBackMotionEvent(edge, touchX, touchY, caclulateProgress())); 187 ShadowLooper.idleMainLooper(); 188 } catch (RemoteException e) { 189 throw new RuntimeException(e); 190 } 191 } 192 193 /** Cancels the back gesture. */ cancel()194 public void cancel() { 195 checkState(!isCancelled && !isFinished); 196 isCancelled = true; 197 try { 198 ShadowWindowManagerGlobal.windowSessionDelegate 199 .onBackInvokedCallbackInfo 200 .getCallback() 201 .onBackCancelled(); 202 ShadowWindowManagerGlobal.windowSessionDelegate.currentPredictiveBackGesture = null; 203 ShadowLooper.idleMainLooper(); 204 } catch (RemoteException e) { 205 throw new RuntimeException(e); 206 } 207 } 208 209 /** 210 * Ends the back gesture. If the back gesture has not been cancelled by calling {@link 211 * #cancel()} then the back handler is invoked. 212 * 213 * <p>Callers should always call either {@link #cancel()} or {@link #close()}. It is recommended 214 * to use the result of {@link ShadowWindowManagerGlobal#startPredictiveBackGesture} in a try 215 * with resources. 216 */ 217 @Override close()218 public void close() { 219 checkState(!isFinished); 220 isFinished = true; 221 if (!isCancelled) { 222 try { 223 ShadowWindowManagerGlobal.windowSessionDelegate 224 .onBackInvokedCallbackInfo 225 .getCallback() 226 .onBackInvoked(); 227 ShadowWindowManagerGlobal.windowSessionDelegate.currentPredictiveBackGesture = null; 228 ShadowLooper.idleMainLooper(); 229 } catch (RemoteException e) { 230 throw new RuntimeException(e); 231 } 232 } 233 } 234 caclulateProgress()235 private float caclulateProgress() { 236 // The real implementation anchors the progress on the start x and resets it each time the 237 // threshold is lost, it also calculates a linear and non linear progress area. This 238 // implementation is much simpler. 239 int direction = (edge == BackEvent.EDGE_LEFT ? 1 : -1); 240 float draggableWidth = 241 (edge == BackEvent.EDGE_LEFT ? displayWidth - startTouchX : startTouchX) 242 - progressThreshold; 243 return max((((touchX - startTouchX) * direction) - progressThreshold) / draggableWidth, 0f); 244 } 245 } 246 247 /** 248 * Starts a predictive back gesture in the center of the edge. See {@link 249 * #startPredictiveBackGesture(int, float)}. 250 */ 251 @Nullable startPredictiveBackGesture(@ackEvent.SwipeEdge int edge)252 public static PredictiveBackGesture startPredictiveBackGesture(@BackEvent.SwipeEdge int edge) { 253 return startPredictiveBackGesture(edge, 0.5f); 254 } 255 256 /** 257 * Starts a predictive back gesture. 258 * 259 * <p>If no active activity with a back pressed callback that supports animations is registered 260 * then null will be returned. See {@link PredictiveBackGesture}. 261 * 262 * <p>See {@link ShadowApplication#setEnableOnBackInvokedCallback}. 263 * 264 * @param position The position on edge of the window 265 */ 266 @Nullable startPredictiveBackGesture( @ackEvent.SwipeEdge int edge, @FloatRange(from = 0f, to = 1f) float position)267 public static PredictiveBackGesture startPredictiveBackGesture( 268 @BackEvent.SwipeEdge int edge, @FloatRange(from = 0f, to = 1f) float position) { 269 checkArgument(position >= 0f && position <= 1f, "Invalid position: %s.", position); 270 checkState( 271 windowSessionDelegate.currentPredictiveBackGesture == null, 272 "Current predictive back gesture in progress."); 273 if (windowSessionDelegate.onBackInvokedCallbackInfo == null 274 || !windowSessionDelegate.onBackInvokedCallbackInfo.isAnimationCallback()) { 275 return null; 276 } else { 277 try { 278 // Exclusion rects are sent to the window session by posting so idle the looper first. 279 ShadowLooper.idleMainLooper(); 280 int touchSlop = 281 ViewConfiguration.get(RuntimeEnvironment.getApplication()).getScaledTouchSlop(); 282 int displayWidth = ShadowDisplay.getDefaultDisplay().getWidth(); 283 float deltaX = (edge == BackEvent.EDGE_LEFT ? 1 : -1) * touchSlop / 2f; 284 float downX = (edge == BackEvent.EDGE_LEFT ? 0 : displayWidth) + deltaX; 285 float downY = ShadowDisplay.getDefaultDisplay().getHeight() * position; 286 if (windowSessionDelegate.systemGestureExclusionRects != null) { 287 // TODO: The rects should be offset based on the window's position in the display, most 288 // windows should be full screen which makes this naive logic work ok. 289 for (Rect rect : windowSessionDelegate.systemGestureExclusionRects) { 290 if (rect.contains(round(downX), round(downY))) { 291 return null; 292 } 293 } 294 } 295 // A predictive back gesture starts as a user swipe which the window will receive the start 296 // of the gesture before it gets intercepted by the window manager. 297 MotionEvent downEvent = 298 MotionEvent.obtain( 299 /* downTime= */ SystemClock.uptimeMillis(), 300 /* eventTime= */ SystemClock.uptimeMillis(), 301 MotionEvent.ACTION_DOWN, 302 downX, 303 downY, 304 /* metaState= */ 0); 305 MotionEvent moveEvent = MotionEvent.obtain(downEvent); 306 moveEvent.setAction(MotionEvent.ACTION_MOVE); 307 moveEvent.offsetLocation(deltaX, 0); 308 MotionEvent cancelEvent = MotionEvent.obtain(moveEvent); 309 cancelEvent.setAction(MotionEvent.ACTION_CANCEL); 310 ShadowUiAutomation.injectInputEvent(downEvent); 311 ShadowUiAutomation.injectInputEvent(moveEvent); 312 ShadowUiAutomation.injectInputEvent(cancelEvent); 313 windowSessionDelegate 314 .onBackInvokedCallbackInfo 315 .getCallback() 316 .onBackStarted( 317 BackMotionEvents.newBackMotionEvent( 318 edge, downX + 2 * deltaX, downY, /* progress= */ 0)); 319 ShadowLooper.idleMainLooper(); 320 PredictiveBackGesture backGesture = 321 new PredictiveBackGesture(edge, displayWidth, downX + 2 * deltaX, downY); 322 windowSessionDelegate.currentPredictiveBackGesture = backGesture; 323 return backGesture; 324 } catch (RemoteException e) { 325 Log.e("ShadowWindowManagerGlobal", "Failed to start back gesture", e); 326 return null; 327 } 328 } 329 } 330 331 @SuppressWarnings("unchecked") // Cast args to IWindowSession methods 332 @Implementation getWindowSession()333 protected static synchronized IWindowSession getWindowSession() { 334 if (windowSession == null) { 335 // Use Proxy.newProxyInstance instead of ReflectionHelpers.createDelegatingProxy as there are 336 // too many variants of 'add', 'addToDisplay', and 'addToDisplayAsUser', some of which have 337 // arg types that don't exist any more. 338 windowSession = 339 (IWindowSession) 340 Proxy.newProxyInstance( 341 IWindowSession.class.getClassLoader(), 342 new Class<?>[] {IWindowSession.class}, 343 (proxy, method, args) -> { 344 String methodName = method.getName(); 345 switch (methodName) { 346 case "add": // SDK 16 347 case "addToDisplay": // SDK 17-29 348 case "addToDisplayAsUser": // SDK 30+ 349 return windowSessionDelegate.addToDisplay(args); 350 case "remove": 351 windowSessionDelegate.remove(args); 352 return null; 353 case "relayout": 354 return windowSessionDelegate.relayout(args); 355 case "getInTouchMode": 356 return windowSessionDelegate.getInTouchMode(); 357 case "performDrag": 358 return windowSessionDelegate.performDrag(args); 359 case "prepareDrag": 360 return windowSessionDelegate.prepareDrag(); 361 case "setInTouchMode": 362 windowSessionDelegate.setInTouchMode((boolean) args[0]); 363 return null; 364 case "setOnBackInvokedCallbackInfo": 365 windowSessionDelegate.onBackInvokedCallbackInfo = 366 (OnBackInvokedCallbackInfo) args[1]; 367 return null; 368 case "reportSystemGestureExclusionChanged": 369 windowSessionDelegate.systemGestureExclusionRects = (List<Rect>) args[1]; 370 return null; 371 case "insetsModified": 372 case "updateRequestedVisibilities": 373 case "updateRequestedVisibleTypes": 374 windowSessionDelegate.updateInsets(args); 375 return null; 376 default: 377 return ReflectionHelpers.defaultValueForType( 378 method.getReturnType().getName()); 379 } 380 }); 381 } 382 return windowSession; 383 } 384 385 @Implementation peekWindowSession()386 protected static synchronized IWindowSession peekWindowSession() { 387 return windowSession; 388 } 389 390 @Implementation getWindowManagerService()391 public static @ClassName("android.view.IWindowManager") Object getWindowManagerService() 392 throws RemoteException { 393 IWindowManager service = 394 reflector(WindowManagerGlobalReflector.class).getWindowManagerService(); 395 if (service == null) { 396 service = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)); 397 reflector(WindowManagerGlobalReflector.class).setWindowManagerService(service); 398 } 399 return service; 400 } 401 402 @ForType(WindowManagerGlobal.class) 403 interface WindowManagerGlobalReflector { 404 @Accessor("sDefaultWindowManager") 405 @Static setDefaultWindowManager(WindowManagerGlobal global)406 void setDefaultWindowManager(WindowManagerGlobal global); 407 408 @Static 409 @Accessor("sWindowManagerService") getWindowManagerService()410 IWindowManager getWindowManagerService(); 411 412 @Static 413 @Accessor("sWindowManagerService") setWindowManagerService(IWindowManager service)414 void setWindowManagerService(IWindowManager service); 415 416 @Accessor("mViews") getWindowViews()417 List<View> getWindowViews(); 418 } 419 420 private static class WindowSessionDelegate { 421 private final LinkedHashMap<IWindow, WindowInfo> windows = new LinkedHashMap<>(); 422 423 // From WindowManagerGlobal (was WindowManagerImpl in JB). 424 static final int ADD_FLAG_IN_TOUCH_MODE = 0x1; 425 static final int ADD_FLAG_APP_VISIBLE = 0x2; 426 static final int RELAYOUT_RES_IN_TOUCH_MODE = 0x1; 427 428 // TODO: Default to touch mode always. 429 private boolean inTouchMode = useRealGraphics(); 430 @Nullable protected ClipData lastDragClipData; 431 @Nullable private OnBackInvokedCallbackInfo onBackInvokedCallbackInfo; 432 @Nullable private List<Rect> systemGestureExclusionRects; 433 @Nullable private PredictiveBackGesture currentPredictiveBackGesture; 434 addToDisplay(Object[] args)435 protected int addToDisplay(Object[] args) { 436 int sdk = RuntimeEnvironment.getApiLevel(); 437 WindowInfo windowInfo = windows.computeIfAbsent((IWindow) args[0], id -> new WindowInfo()); 438 int displayId = (int) args[sdk <= R ? 4 : 3]; 439 // TODO: This is to allow window insets to be keyed per display, i.e. a window has requested 440 // insets visibility changed before it was added to a display, does android actually allow 441 // per display window inset visibilities? 442 if (sdk >= R) { 443 applyInsets(displayId, windowInfo.requestedVisibleTypes); 444 } 445 windowInfo.displayId = displayId; 446 447 // Create some insets source controls otherwise the insets controller will not apply changes. 448 if (sdk >= R && sdk < UPSIDE_DOWN_CAKE) { 449 populateInsetSourceControls(windowInfo, findFirst(InsetsSourceControl[].class, args)); 450 windowInfo.hasInsetsControl = true; 451 transferWindowInsetsControlTo(windowInfo); 452 } 453 Rect[] rects = findAll(Rect.class, args); 454 int rectIdx = 0; 455 configureWindowFrames( 456 windowInfo, 457 /* inAttrs= */ (WindowManager.LayoutParams) args[sdk <= R ? 2 : 1], 458 /* requestedSize= */ null, 459 /* outFrame= */ sdk >= P && rects.length > rectIdx ? rects[rectIdx++] : null, 460 /* outContentInsets= */ sdk <= R ? rects[rectIdx++] : null, 461 /* outVisibleInsets= */ null, 462 /* outStableInsets= */ sdk >= LOLLIPOP_MR1 && sdk <= R ? rects[rectIdx] : null, 463 /* outInsetsState= */ sdk >= Q ? findFirst(InsetsState.class, args) : null); 464 465 int res = 0; 466 // Temporarily enable this based on a system property to allow for test migration. This will 467 // eventually be updated to default and true and eventually removed, Robolectric's previous 468 // behavior of not marking windows as visible by default is a bug. This flag should only be 469 // used as a temporary toggle during migration. 470 if (useRealGraphics() 471 || "true".equals(System.getProperty("robolectric.areWindowsMarkedVisible", "false"))) { 472 res |= ADD_FLAG_APP_VISIBLE; 473 } 474 res |= inTouchMode ? ADD_FLAG_IN_TOUCH_MODE : 0; 475 return res; 476 } 477 remove(Object[] args)478 protected void remove(Object[] args) { 479 IWindow window = (IWindow) args[0]; 480 windows.remove(window); 481 // TODO: This transfers control to the last window, should there be another heuristic here? 482 // TODO: Streams.findLast is not available in Android Guava yet. 483 transferWindowInsetsControlTo(windows.values().stream().reduce((a, b) -> b).orElse(null)); 484 } 485 relayout(Object[] args)486 protected int relayout(Object[] args) { 487 int sdk = RuntimeEnvironment.getApiLevel(); 488 WindowRelayoutResult windowLayoutResult = 489 sdk >= VANILLA_ICE_CREAM ? findFirst(WindowRelayoutResult.class, args) : null; 490 491 // Simulate initializing the SurfaceControl member object, which happens during this method. 492 if (sdk >= Q) { 493 SurfaceControl surfaceControl = 494 sdk >= VANILLA_ICE_CREAM 495 ? windowLayoutResult.surfaceControl 496 : findFirst(SurfaceControl.class, args); 497 Shadow.<ShadowSurfaceControl>extract(surfaceControl).initializeNativeObject(); 498 } 499 500 IWindow window = (IWindow) args[0]; 501 WindowInfo windowInfo = windows.get(window); 502 // In legacy looper mode relayout can be called out of order with add so just ignore it. 503 // TODO: In paused looper mode the material SnackbarManager static instance leaks state 504 // between tests and triggers relayout on window roots that are cleared, for now just ignore 505 // them here, but ideally this library would not leak state between tests. 506 if (windowInfo != null) { 507 if (sdk >= R && sdk < UPSIDE_DOWN_CAKE) { 508 InsetsSourceControl[] controls = findFirst(InsetsSourceControl[].class, args); 509 if (windowInfo.hasInsetsControl) { 510 populateInsetSourceControls(windowInfo, controls); 511 } else { 512 Arrays.setAll(controls, i -> null); 513 } 514 } 515 Rect[] rects = findAll(Rect.class, args); 516 int requestedSizeIdx = sdk < S ? 3 : 2; 517 configureWindowFrames( 518 checkNotNull(windowInfo), 519 /* inAttrs= */ (WindowManager.LayoutParams) args[sdk <= R ? 2 : 1], 520 /* requestedSize= */ new Point( 521 (int) args[requestedSizeIdx], (int) args[requestedSizeIdx + 1]), 522 /* outFrame= */ rects.length > 0 523 ? rects[0] 524 : (windowLayoutResult != null 525 ? windowLayoutResult.frames 526 : findFirst(ClientWindowFrames.class, args)) 527 .frame, 528 /* outContentInsets= */ sdk <= R ? rects[2] : null, 529 /* outVisibleInsets= */ sdk <= R ? rects[3] : null, 530 /* outStableInsets= */ sdk <= R ? rects[4] : null, 531 /* outInsetsState= */ sdk >= Q 532 ? (windowLayoutResult != null 533 ? windowLayoutResult.insetsState 534 : findFirst(InsetsState.class, args)) 535 : null); 536 } 537 538 return inTouchMode ? RELAYOUT_RES_IN_TOUCH_MODE : 0; 539 } 540 541 private void configureWindowFrames( 542 WindowInfo windowInfo, 543 @Nullable WindowManager.LayoutParams inAttrs, 544 Point requestedSize, 545 Rect outFrame, 546 Rect outContentInsets, 547 Rect outVisibleInsets, 548 Rect outStableInsets, 549 InsetsState outInsetsState) { 550 SystemUi systemUi = systemUiForDisplay(windowInfo.displayId); 551 DisplayInfo displayInfo = 552 DisplayManagerGlobal.getInstance().getDisplayInfo(windowInfo.displayId); 553 WindowManager.LayoutParams attrs = windowInfo.attrs; 554 if (inAttrs != null) { 555 attrs.copyFrom(inAttrs); 556 } 557 windowInfo.displayFrame.set(0, 0, displayInfo.logicalWidth, displayInfo.logicalHeight); 558 Rect contentFrame = new Rect(windowInfo.displayFrame); 559 systemUi.adjustFrameForInsets(attrs, contentFrame); 560 // TODO: Remove this and respect the requested size as real Android does. For back compat 561 // reasons temporarily ignore requested size. 562 boolean useRequestedSize = Boolean.getBoolean("robolectric.windowManager.useRequestedSize"); 563 int width = 564 useRequestedSize && requestedSize != null && attrs.width != LayoutParams.MATCH_PARENT 565 ? requestedSize.x 566 : (attrs.width > 0 ? attrs.width : contentFrame.width()); 567 int height = 568 useRequestedSize && requestedSize != null && attrs.height != LayoutParams.MATCH_PARENT 569 ? requestedSize.y 570 : (attrs.height > 0 ? attrs.height : contentFrame.height()); 571 // TODO: Take account of parent frame for child windows. 572 Gravity.apply( 573 attrs.gravity, 574 width, 575 height, 576 contentFrame, 577 (int) (attrs.x + attrs.horizontalMargin * contentFrame.width()), 578 (int) (attrs.y + attrs.verticalMargin * contentFrame.height()), 579 windowInfo.frame); 580 if (!useRequestedSize) { 581 // If we are not respecting the requested size, for backwards compatibility allow the window 582 // to offset to the requested position ignoring the gravity and display bounds. 583 windowInfo.frame.offsetTo(attrs.x, attrs.y); 584 } else { 585 Gravity.applyDisplay(attrs.gravity, contentFrame, windowInfo.frame); 586 } 587 systemUiForDisplay(windowInfo.displayId).putInsets(windowInfo); 588 windowInfo.put(outFrame, outContentInsets, outVisibleInsets, outStableInsets, outInsetsState); 589 } 590 591 public boolean getInTouchMode() { 592 return inTouchMode; 593 } 594 595 public void setInTouchMode(boolean inTouchMode) { 596 this.inTouchMode = inTouchMode; 597 } 598 599 public IBinder prepareDrag() { 600 return new Binder(); 601 } 602 603 public Object performDrag(Object[] args) { 604 // extract the clipData param 605 for (int i = args.length - 1; i >= 0; i--) { 606 if (args[i] instanceof ClipData) { 607 lastDragClipData = (ClipData) args[i]; 608 // In P (SDK 28), the return type changed from boolean to Binder. 609 return RuntimeEnvironment.getApiLevel() >= P ? new Binder() : true; 610 } 611 } 612 throw new AssertionError("Missing ClipData param"); 613 } 614 updateInsets(Object[] args)615 public void updateInsets(Object[] args) { 616 int sdk = RuntimeEnvironment.getApiLevel(); 617 checkState(sdk >= R); 618 IWindow window = (IWindow) args[0]; 619 WindowInfo windowInfo = windows.computeIfAbsent(window, id -> new WindowInfo()); 620 if (sdk <= S) { 621 InsetsState state = (InsetsState) args[1]; 622 InsetsSource statusBar = state.peekSource(ShadowInsetsState.STATUS_BARS); 623 InsetsSource navBar = state.peekSource(ShadowInsetsState.NAVIGATION_BARS); 624 windowInfo.requestedVisibleTypes = 625 (statusBar == null || statusBar.isVisible() ? statusBars() : 0) 626 | (navBar == null || navBar.isVisible() ? navigationBars() : 0); 627 } else if (sdk <= TIRAMISU) { 628 InsetsVisibilitiesReflector visibilities = 629 reflector(InsetsVisibilitiesReflector.class, args[1]); 630 boolean statusBar = visibilities.getVisibility(ShadowInsetsState.STATUS_BARS); 631 boolean navBar = visibilities.getVisibility(ShadowInsetsState.NAVIGATION_BARS); 632 windowInfo.requestedVisibleTypes = 633 (statusBar ? statusBars() : 0) | (navBar ? navigationBars() : 0); 634 } else { 635 windowInfo.requestedVisibleTypes = (int) args[1]; 636 } 637 if (windowInfo.displayId != -1) { 638 applyInsets(windowInfo.displayId, windowInfo.requestedVisibleTypes); 639 } 640 } 641 applyInsets(int displayId, int requestedVisibleTypes)642 void applyInsets(int displayId, int requestedVisibleTypes) { 643 checkState(displayId != -1); 644 SystemUi systemUi = systemUiForDisplay(displayId); 645 boolean statusBarVisible = (requestedVisibleTypes & statusBars()) != 0; 646 if (systemUi.getStatusBar().isVisible() != statusBarVisible) { 647 systemUi.getStatusBar().setVisible(statusBarVisible); 648 notifyInsetsChanges(systemUi, systemUi.getStatusBar().getId()); 649 } 650 boolean navBarVisible = (requestedVisibleTypes & navigationBars()) != 0; 651 if (systemUi.getNavigationBar().isVisible() != navBarVisible) { 652 systemUi.getNavigationBar().setVisible(navBarVisible); 653 notifyInsetsChanges(systemUi, systemUi.getNavigationBar().getId()); 654 } 655 } 656 notifyInsetsChanges(SystemUi systemUi, @Nullable Integer type)657 void notifyInsetsChanges(SystemUi systemUi, @Nullable Integer type) { 658 for (Entry<IWindow, WindowInfo> windowEntry : windows.entrySet()) { 659 if (windowEntry.getValue().displayId == systemUi.getDisplayId()) { 660 systemUi.putInsets(windowEntry.getValue()); 661 sendInsetsControlChanged(windowEntry.getKey(), type, false); 662 // TODO: only send resize if the window resized. 663 sendResize(windowEntry.getKey()); 664 } 665 } 666 } 667 sendInsetsControlChanged( IWindow window, @Nullable Integer type, boolean hasControlsChanged)668 void sendInsetsControlChanged( 669 IWindow window, @Nullable Integer type, boolean hasControlsChanged) { 670 int sdk = RuntimeEnvironment.getApiLevel(); 671 WindowInfo windowInfo = checkNotNull(windows.get(window)); 672 InsetsState insetsState = new InsetsState(windowInfo.insetsState); 673 // On R if we don't remove the sources that aren't changing we'll infinite loop when toggling 674 // visibility of multiple bars. 675 if (sdk == R && type != null) { 676 for (int i = 0; i < Shadow.<ShadowInsetsState>extract(insetsState).getSourceSize(); i++) { 677 if (i != type) { 678 insetsState.removeSource(i); 679 } 680 } 681 } 682 if ((sdk == R && !hasControlsChanged) || sdk >= S && sdk <= S_V2) { 683 ClassParameterBuilder params = new ClassParameterBuilder(); 684 params.add(InsetsState.class, windowInfo.insetsState); 685 /* willMove */ params.addIf(sdk >= S, boolean.class, false); 686 /* willResize */ params.addIf(sdk >= S, boolean.class, false); 687 callInstanceMethod(window, "insetsChanged", params.build()); 688 } else { 689 ClassParameterBuilder params = new ClassParameterBuilder(); 690 params.add(InsetsState.class, windowInfo.insetsState); 691 // TODO: We should give control to the active window. 692 if (sdk >= VANILLA_ICE_CREAM) { 693 params.add(InsetsSourceControl.Array.class, new InsetsSourceControl.Array()); 694 } else { 695 params.add( 696 InsetsSourceControl[].class, 697 windowInfo.hasInsetsControl ? populateInsetSourceControls(windowInfo, null) : null); 698 } 699 callInstanceMethod(window, "insetsControlChanged", params.build()); 700 } 701 } 702 sendResize(IWindow window)703 void sendResize(IWindow window) { 704 int sdk = RuntimeEnvironment.getApiLevel(); 705 WindowInfo windowInfo = checkNotNull(windows.get(window)); 706 configureWindowFrames( 707 windowInfo, 708 windowInfo.attrs, 709 /* requestedSize= */ null, 710 /* outFrame= */ null, 711 /* outContentInsets= */ null, 712 /* outVisibleInsets= */ null, 713 /* outStableInsets= */ null, 714 /* outInsetsState= */ null); 715 Configuration configuration = 716 RuntimeEnvironment.getApplication().getResources().getConfiguration(); 717 ClassParameterBuilder args = new ClassParameterBuilder(); 718 719 // The resized method has changed pretty much every other release, this is a canonicalize-d 720 // set of all the parameters it has ever taken. 721 if (sdk >= S) { 722 /* frames */ args.add(ClientWindowFrames.class, windowInfo.frames); 723 } else { 724 /* frame */ args.add(Rect.class, windowInfo.frame); 725 /* overscanInsets */ args.addIf(sdk <= Q, Rect.class, new Rect()); 726 /* contentInsets */ args.add(Rect.class, windowInfo.contentInsets); 727 /* visibleInsets */ args.add(Rect.class, windowInfo.visibleInsets); 728 /* stableInsets */ args.add(Rect.class, windowInfo.stableInsets); 729 /* outsets */ args.addIf(sdk >= M && sdk <= Q, Rect.class, new Rect()); 730 } 731 /* reportDraw */ args.add(boolean.class, false); 732 if (sdk <= N_MR1) { 733 /* newConfig */ args.add(Configuration.class, configuration); 734 } else { 735 /* newMergedConfiguration */ args.add( 736 MergedConfiguration.class, new MergedConfiguration(configuration)); 737 } 738 /* backDropFrame */ args.addIf(sdk >= N && sdk <= R, Rect.class, new Rect()); 739 if (sdk >= TIRAMISU) { 740 /* insetsState */ args.add(InsetsState.class, windowInfo.insetsState); 741 } 742 /* forceLayout */ args.addIf(sdk >= N, boolean.class, false); 743 /* alwaysConsumeNavBar */ args.addIf(sdk >= N, boolean.class, false); 744 /* displayId */ args.addIf(sdk >= O, int.class, windowInfo.displayId); 745 if (sdk >= P && sdk <= R) { 746 /* displayCutout */ args.add( 747 DisplayCutout.ParcelableWrapper.class, new DisplayCutout.ParcelableWrapper()); 748 } 749 /* syncSeqId */ args.addIf(sdk >= TIRAMISU, int.class, 0); 750 /* resizeMode */ args.addIf(sdk == TIRAMISU, int.class, 0); 751 /* dragResizing */ args.addIf(sdk >= UPSIDE_DOWN_CAKE, boolean.class, false); 752 if (sdk > UPSIDE_DOWN_CAKE) { 753 /* activityWindowInfo */ args.add(ActivityWindowInfo.class, null); 754 } 755 callInstanceMethod(window, "resized", args.build()); 756 } 757 transferWindowInsetsControlTo(WindowInfo windowInfo)758 private void transferWindowInsetsControlTo(WindowInfo windowInfo) { 759 // If we don't transfer the controls on R then windows conflict when their insets mismatch, 760 // resulting in infinite loops, and if no window has control then insets are not updated. 761 // TODO: This is almost certainly not the correct logic for determining which window has 762 // control. 763 if (RuntimeEnvironment.getApiLevel() != R) { 764 return; 765 } 766 for (Entry<IWindow, WindowInfo> entry : windows.entrySet()) { 767 boolean hasControl = entry.getValue() == windowInfo; 768 if (entry.getValue().hasInsetsControl != hasControl) { 769 entry.getValue().hasInsetsControl = hasControl; 770 sendInsetsControlChanged(entry.getKey(), null, true); 771 } 772 } 773 } 774 775 @CanIgnoreReturnValue populateInsetSourceControls( WindowInfo windowInfo, InsetsSourceControl[] controls)776 private InsetsSourceControl[] populateInsetSourceControls( 777 WindowInfo windowInfo, InsetsSourceControl[] controls) { 778 int sdk = RuntimeEnvironment.getApiLevel(); 779 // Skip bars after IME as they have the same public types as navigation/status bars and 780 // their visibility combines. 781 int lastControl = reflector(InsetsStateReflector.class).getImeType(); 782 if (controls == null) { 783 controls = new InsetsSourceControl[lastControl + 1]; 784 } 785 for (int i = 0; i <= lastControl; i++) { 786 ClassParameterBuilder params = new ClassParameterBuilder(); 787 /* type */ params.add(int.class, i); 788 /* leash */ params.add(SurfaceControl.class, null); 789 /* surfacePosition */ params.add(Point.class, new Point()); 790 /* insetsHint */ params.addIf(sdk >= S, Insets.class, Insets.of(0, 0, 0, 0)); 791 controls[i] = callConstructor(InsetsSourceControl.class, params.build()); 792 // Populate the same insets as we did controls, otherwise the insets controller can 793 // infinite loop as it sees the insets being added and removed every time. 794 Shadow.<ShadowInsetsState>extract(windowInfo.insetsState).getOrCreateSource(i); 795 } 796 return controls; 797 } 798 } 799 800 static final class WindowInfo { 801 final Rect displayFrame = new Rect(); 802 final ClientWindowFrames frames; 803 final Rect frame; 804 final InsetsState insetsState = 805 RuntimeEnvironment.getApiLevel() >= Q ? new InsetsState() : null; 806 final Rect contentInsets = new Rect(); 807 final Rect visibleInsets = new Rect(); 808 final Rect stableInsets = new Rect(); 809 final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(); 810 int displayId = -1; 811 int requestedVisibleTypes = RuntimeEnvironment.getApiLevel() >= R ? systemBars() : 0; 812 boolean hasInsetsControl; 813 WindowInfo()814 WindowInfo() { 815 if (RuntimeEnvironment.getApiLevel() >= S) { 816 frames = new ClientWindowFrames(); 817 frame = frames.frame; 818 } else { 819 frames = null; 820 frame = new Rect(); 821 } 822 } 823 put( Rect outFrame, Rect outContentInsets, Rect outVisibleInsets, Rect outStableInsets, InsetsState outInsetsState)824 void put( 825 Rect outFrame, 826 Rect outContentInsets, 827 Rect outVisibleInsets, 828 Rect outStableInsets, 829 InsetsState outInsetsState) { 830 if (outFrame != null) { 831 outFrame.set(frame); 832 } 833 if (outContentInsets != null) { 834 outContentInsets.set(contentInsets); 835 } 836 if (outVisibleInsets != null) { 837 outVisibleInsets.set(visibleInsets); 838 } 839 if (outStableInsets != null) { 840 outStableInsets.set(stableInsets); 841 } 842 if (outInsetsState != null) { 843 outInsetsState.set(insetsState, /* copySources= */ true); 844 } 845 } 846 } 847 findFirst(Class<T> type, Object[] args)848 private static <T> T findFirst(Class<T> type, Object[] args) { 849 return type.cast(stream(args).filter(type::isInstance).findFirst().get()); 850 } 851 findAll(Class<T> type, Object[] args)852 private static <T> T[] findAll(Class<T> type, Object[] args) { 853 return stream(args).filter(type::isInstance).toArray(len -> (T[]) Array.newInstance(type, len)); 854 } 855 856 private static final class ClassParameterBuilder { 857 private final List<ClassParameter<?>> parameters = new ArrayList<>(); 858 add(Class<T> type, T parameter)859 <T> void add(Class<T> type, T parameter) { 860 parameters.add(ClassParameter.from(type, parameter)); 861 } 862 addIf(boolean shouldAdd, Class<T> type, T parameter)863 <T> void addIf(boolean shouldAdd, Class<T> type, T parameter) { 864 if (shouldAdd) { 865 add(type, parameter); 866 } 867 } 868 build()869 ClassParameter<?>[] build() { 870 return parameters.toArray(new ClassParameter<?>[0]); 871 } 872 } 873 874 @ForType(BackMotionEvent.class) 875 interface BackMotionEventReflector { 876 @Constructor newBackMotionEvent( float touchX, float touchY, float progress, float velocityX, float velocityY, int swipeEdge, RemoteAnimationTarget departingAnimationTarget)877 BackMotionEvent newBackMotionEvent( 878 float touchX, 879 float touchY, 880 float progress, 881 float velocityX, 882 float velocityY, 883 int swipeEdge, 884 RemoteAnimationTarget departingAnimationTarget); 885 886 @Constructor newBackMotionEventV( float touchX, float touchY, float progress, float velocityX, float velocityY, boolean triggerBack, int swipeEdge, RemoteAnimationTarget departingAnimationTarget)887 BackMotionEvent newBackMotionEventV( 888 float touchX, 889 float touchY, 890 float progress, 891 float velocityX, 892 float velocityY, 893 boolean triggerBack, 894 int swipeEdge, 895 RemoteAnimationTarget departingAnimationTarget); 896 897 @Constructor newBackMotionEventPostV( float touchX, float touchY, long frameTime, float progress, boolean triggerBack, int swipeEdge, RemoteAnimationTarget departingAnimationTarget)898 BackMotionEvent newBackMotionEventPostV( 899 float touchX, 900 float touchY, 901 long frameTime, 902 float progress, 903 boolean triggerBack, 904 int swipeEdge, 905 RemoteAnimationTarget departingAnimationTarget); 906 } 907 908 @ForType(className = "android.view.InsetsVisibilities") 909 interface InsetsVisibilitiesReflector { getVisibility(int type)910 boolean getVisibility(int type); 911 } 912 913 private static class BackMotionEvents { BackMotionEvents()914 private BackMotionEvents() {} 915 newBackMotionEvent( @ackEvent.SwipeEdge int edge, float touchX, float touchY, float progress)916 static BackMotionEvent newBackMotionEvent( 917 @BackEvent.SwipeEdge int edge, float touchX, float touchY, float progress) { 918 if (RuntimeEnvironment.getApiLevel() < VANILLA_ICE_CREAM) { 919 return reflector(BackMotionEventReflector.class) 920 .newBackMotionEvent( 921 touchX, touchY, progress, 0f, // velocity x 922 0f, // velocity y 923 edge, // swipe edge 924 null); 925 } 926 // normally we would consistently determine which constructor to call based on API level, 927 // but that is tricky for in development SDKS. So just determine 928 // what constructor to call based on the constructors we find reflectively 929 java.lang.reflect.Constructor<?> theConstructor = findPublicConstructor(); 930 if (theConstructor.getParameterTypes()[2].equals(float.class)) { 931 return reflector(BackMotionEventReflector.class) 932 .newBackMotionEventV( 933 touchX, 934 touchY, 935 progress, 936 0f, // velocity x 937 0f, // velocity y 938 Boolean.FALSE, // trigger back 939 edge, // swipe edge 940 null); 941 } else if (theConstructor.getParameterTypes()[2].equals(long.class)) { 942 return reflector(BackMotionEventReflector.class) 943 .newBackMotionEventPostV( 944 touchX, 945 touchY, 946 SystemClock.uptimeMillis(), /* frameTime */ 947 progress, 948 Boolean.FALSE, // trigger back 949 edge, // swipe edge 950 null); 951 } else { 952 throw new IllegalStateException("Could not find a BackMotionEvent constructor to call"); 953 } 954 } 955 findPublicConstructor()956 private static java.lang.reflect.Constructor<?> findPublicConstructor() { 957 for (java.lang.reflect.Constructor<?> constructor : 958 BackMotionEvent.class.getDeclaredConstructors()) { 959 if (constructor.getParameterCount() > 0 && Modifier.isPublic(constructor.getModifiers())) { 960 return constructor; 961 } 962 } 963 throw new IllegalStateException("Could not find a BackMotionEvent constructor"); 964 } 965 } 966 } 967