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