• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.robolectric.shadows;
2 
3 import static android.os.Build.VERSION_CODES.P;
4 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
5 import static com.google.common.base.Preconditions.checkArgument;
6 import static com.google.common.base.Preconditions.checkState;
7 import static java.lang.Math.max;
8 import static java.lang.Math.round;
9 import static org.robolectric.shadows.ShadowView.useRealGraphics;
10 import static org.robolectric.util.reflector.Reflector.reflector;
11 
12 import android.annotation.FloatRange;
13 import android.annotation.Nullable;
14 import android.app.Instrumentation;
15 import android.content.ClipData;
16 import android.content.Context;
17 import android.graphics.Rect;
18 import android.os.Binder;
19 import android.os.IBinder;
20 import android.os.RemoteException;
21 import android.os.ServiceManager;
22 import android.os.SystemClock;
23 import android.util.Log;
24 import android.view.IWindowManager;
25 import android.view.IWindowSession;
26 import android.view.MotionEvent;
27 import android.view.RemoteAnimationTarget;
28 import android.view.View;
29 import android.view.ViewConfiguration;
30 import android.view.WindowManagerGlobal;
31 import android.window.BackEvent;
32 import android.window.BackMotionEvent;
33 import android.window.OnBackInvokedCallbackInfo;
34 import java.io.Closeable;
35 import java.lang.reflect.Proxy;
36 import java.util.List;
37 import org.robolectric.RuntimeEnvironment;
38 import org.robolectric.annotation.Implementation;
39 import org.robolectric.annotation.Implements;
40 import org.robolectric.annotation.Resetter;
41 import org.robolectric.util.ReflectionHelpers;
42 import org.robolectric.util.reflector.Accessor;
43 import org.robolectric.util.reflector.Constructor;
44 import org.robolectric.util.reflector.ForType;
45 import org.robolectric.util.reflector.Static;
46 
47 /** Shadow for {@link WindowManagerGlobal}. */
48 @SuppressWarnings("unused") // Unused params are implementations of Android SDK methods.
49 @Implements(value = WindowManagerGlobal.class, isInAndroidSdk = false, looseSignatures = true)
50 public class ShadowWindowManagerGlobal {
51   private static WindowSessionDelegate windowSessionDelegate = new WindowSessionDelegate();
52   private static IWindowSession windowSession;
53 
54   @Resetter
reset()55   public static void reset() {
56     reflector(WindowManagerGlobalReflector.class).setDefaultWindowManager(null);
57     windowSessionDelegate = new WindowSessionDelegate();
58     windowSession = null;
59   }
60 
getInTouchMode()61   public static boolean getInTouchMode() {
62     return windowSessionDelegate.getInTouchMode();
63   }
64 
65   /**
66    * Sets whether the window manager is in touch mode. Use {@link
67    * Instrumentation#setInTouchMode(boolean)} to modify this from a test.
68    */
setInTouchMode(boolean inTouchMode)69   static void setInTouchMode(boolean inTouchMode) {
70     windowSessionDelegate.setInTouchMode(inTouchMode);
71   }
72 
73   /**
74    * Returns the last {@link ClipData} passed to a drag initiated from a call to {@link
75    * View#startDrag} or {@link View#startDragAndDrop}, or null if there isn't one.
76    */
77   @Nullable
getLastDragClipData()78   public static ClipData getLastDragClipData() {
79     return windowSessionDelegate.lastDragClipData;
80   }
81 
82   /** Clears the data returned by {@link #getLastDragClipData()}. */
clearLastDragClipData()83   public static void clearLastDragClipData() {
84     windowSessionDelegate.lastDragClipData = null;
85   }
86 
87   /**
88    * Ongoing predictive back gesture.
89    *
90    * <p>Start a predictive back gesture by calling {@link
91    * ShadowWindowManagerGlobal#startPredictiveBackGesture}. One or more drag progress events can be
92    * dispatched by calling {@link #moveBy}. The gesture must be ended by either calling {@link
93    * #cancel()} or {@link #close()}, if {@link #cancel()} is called a subsequent call to {@link
94    * close()} will do nothing to allow using the gesture in a try with resources statement:
95    *
96    * <pre>
97    * try (PredictiveBackGesture backGesture =
98    *     ShadowWindowManagerGlobal.startPredictiveBackGesture(BackEvent.EDGE_LEFT)) {
99    *   backGesture.moveBy(10, 10);
100    * }
101    * </pre>
102    */
103   public static final class PredictiveBackGesture implements Closeable {
104     @BackEvent.SwipeEdge private final int edge;
105     private final int displayWidth;
106     private final float startTouchX;
107     private final float progressThreshold;
108     private float touchX;
109     private float touchY;
110     private boolean isCancelled;
111     private boolean isFinished;
112 
PredictiveBackGesture( @ackEvent.SwipeEdge int edge, int displayWidth, float touchX, float touchY)113     private PredictiveBackGesture(
114         @BackEvent.SwipeEdge int edge, int displayWidth, float touchX, float touchY) {
115       this.edge = edge;
116       this.displayWidth = displayWidth;
117       this.progressThreshold =
118           ViewConfiguration.get(RuntimeEnvironment.getApplication()).getScaledTouchSlop();
119       this.startTouchX = touchX;
120       this.touchX = touchX;
121       this.touchY = touchY;
122     }
123 
124     /** Dispatches drag progress for a predictive back gesture. */
moveBy(float dx, float dy)125     public void moveBy(float dx, float dy) {
126       checkState(!isCancelled && !isFinished);
127       try {
128         touchX += dx;
129         touchY += dy;
130         ShadowWindowManagerGlobal.windowSessionDelegate
131             .onBackInvokedCallbackInfo
132             .getCallback()
133             .onBackProgressed(
134                 BackMotionEvents.newBackMotionEvent(edge, touchX, touchY, caclulateProgress()));
135         ShadowLooper.idleMainLooper();
136       } catch (RemoteException e) {
137         throw new RuntimeException(e);
138       }
139     }
140 
141     /** Cancels the back gesture. */
cancel()142     public void cancel() {
143       checkState(!isCancelled && !isFinished);
144       isCancelled = true;
145       try {
146         ShadowWindowManagerGlobal.windowSessionDelegate
147             .onBackInvokedCallbackInfo
148             .getCallback()
149             .onBackCancelled();
150         ShadowWindowManagerGlobal.windowSessionDelegate.currentPredictiveBackGesture = null;
151         ShadowLooper.idleMainLooper();
152       } catch (RemoteException e) {
153         throw new RuntimeException(e);
154       }
155     }
156 
157     /**
158      * Ends the back gesture. If the back gesture has not been cancelled by calling {@link
159      * #cancel()} then the back handler is invoked.
160      *
161      * <p>Callers should always call either {@link #cancel()} or {@link #close()}. It is recommended
162      * to use the result of {@link ShadowWindowManagerGlobal#startPredictiveBackGesture} in a try
163      * with resources.
164      */
165     @Override
close()166     public void close() {
167       checkState(!isFinished);
168       isFinished = true;
169       if (!isCancelled) {
170         try {
171           ShadowWindowManagerGlobal.windowSessionDelegate
172               .onBackInvokedCallbackInfo
173               .getCallback()
174               .onBackInvoked();
175           ShadowWindowManagerGlobal.windowSessionDelegate.currentPredictiveBackGesture = null;
176           ShadowLooper.idleMainLooper();
177         } catch (RemoteException e) {
178           throw new RuntimeException(e);
179         }
180       }
181     }
182 
caclulateProgress()183     private float caclulateProgress() {
184       // The real implementation anchors the progress on the start x and resets it each time the
185       // threshold is lost, it also calculates a linear and non linear progress area. This
186       // implementation is much simpler.
187       int direction = (edge == BackEvent.EDGE_LEFT ? 1 : -1);
188       float draggableWidth =
189           (edge == BackEvent.EDGE_LEFT ? displayWidth - startTouchX : startTouchX)
190               - progressThreshold;
191       return max((((touchX - startTouchX) * direction) - progressThreshold) / draggableWidth, 0f);
192     }
193   }
194 
195   /**
196    * Starts a predictive back gesture in the center of the edge. See {@link
197    * #startPredictiveBackGesture(int, float)}.
198    */
199   @Nullable
startPredictiveBackGesture(@ackEvent.SwipeEdge int edge)200   public static PredictiveBackGesture startPredictiveBackGesture(@BackEvent.SwipeEdge int edge) {
201     return startPredictiveBackGesture(edge, 0.5f);
202   }
203 
204   /**
205    * Starts a predictive back gesture.
206    *
207    * <p>If no active activity with a back pressed callback that supports animations is registered
208    * then null will be returned. See {@link PredictiveBackGesture}.
209    *
210    * <p>See {@link ShadowApplication#setEnableOnBackInvokedCallback}.
211    *
212    * @param position The position on edge of the window
213    */
214   @Nullable
startPredictiveBackGesture( @ackEvent.SwipeEdge int edge, @FloatRange(from = 0f, to = 1f) float position)215   public static PredictiveBackGesture startPredictiveBackGesture(
216       @BackEvent.SwipeEdge int edge, @FloatRange(from = 0f, to = 1f) float position) {
217     checkArgument(position >= 0f && position <= 1f, "Invalid position: %s.", position);
218     checkState(
219         windowSessionDelegate.currentPredictiveBackGesture == null,
220         "Current predictive back gesture in progress.");
221     if (windowSessionDelegate.onBackInvokedCallbackInfo == null
222         || !windowSessionDelegate.onBackInvokedCallbackInfo.isAnimationCallback()) {
223       return null;
224     } else {
225       try {
226         // Exclusion rects are sent to the window session by posting so idle the looper first.
227         ShadowLooper.idleMainLooper();
228         int touchSlop =
229             ViewConfiguration.get(RuntimeEnvironment.getApplication()).getScaledTouchSlop();
230         int displayWidth = ShadowDisplay.getDefaultDisplay().getWidth();
231         float deltaX = (edge == BackEvent.EDGE_LEFT ? 1 : -1) * touchSlop / 2f;
232         float downX = (edge == BackEvent.EDGE_LEFT ? 0 : displayWidth) + deltaX;
233         float downY = ShadowDisplay.getDefaultDisplay().getHeight() * position;
234         if (windowSessionDelegate.systemGestureExclusionRects != null) {
235           // TODO: The rects should be offset based on the window's position in the display, most
236           //  windows should be full screen which makes this naive logic work ok.
237           for (Rect rect : windowSessionDelegate.systemGestureExclusionRects) {
238             if (rect.contains(round(downX), round(downY))) {
239               return null;
240             }
241           }
242         }
243         // A predictive back gesture starts as a user swipe which the window will receive the start
244         // of the gesture before it gets intercepted by the window manager.
245         MotionEvent downEvent =
246             MotionEvent.obtain(
247                 /* downTime= */ SystemClock.uptimeMillis(),
248                 /* eventTime= */ SystemClock.uptimeMillis(),
249                 MotionEvent.ACTION_DOWN,
250                 downX,
251                 downY,
252                 /* metaState= */ 0);
253         MotionEvent moveEvent = MotionEvent.obtain(downEvent);
254         moveEvent.setAction(MotionEvent.ACTION_MOVE);
255         moveEvent.offsetLocation(deltaX, 0);
256         MotionEvent cancelEvent = MotionEvent.obtain(moveEvent);
257         cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
258         ShadowUiAutomation.injectInputEvent(downEvent);
259         ShadowUiAutomation.injectInputEvent(moveEvent);
260         ShadowUiAutomation.injectInputEvent(cancelEvent);
261         windowSessionDelegate
262             .onBackInvokedCallbackInfo
263             .getCallback()
264             .onBackStarted(
265                 BackMotionEvents.newBackMotionEvent(
266                     edge, downX + 2 * deltaX, downY, /* progress= */ 0));
267         ShadowLooper.idleMainLooper();
268         PredictiveBackGesture backGesture =
269             new PredictiveBackGesture(edge, displayWidth, downX + 2 * deltaX, downY);
270         windowSessionDelegate.currentPredictiveBackGesture = backGesture;
271         return backGesture;
272       } catch (RemoteException e) {
273         Log.e("ShadowWindowManagerGlobal", "Failed to start back gesture", e);
274         return null;
275       }
276     }
277   }
278 
279   @SuppressWarnings("unchecked") // Cast args to IWindowSession methods
280   @Implementation
getWindowSession()281   protected static synchronized IWindowSession getWindowSession() {
282     if (windowSession == null) {
283       // Use Proxy.newProxyInstance instead of ReflectionHelpers.createDelegatingProxy as there are
284       // too many variants of 'add', 'addToDisplay', and 'addToDisplayAsUser', some of which have
285       // arg types that don't exist any more.
286       windowSession =
287           (IWindowSession)
288               Proxy.newProxyInstance(
289                   IWindowSession.class.getClassLoader(),
290                   new Class<?>[] {IWindowSession.class},
291                   (proxy, method, args) -> {
292                     String methodName = method.getName();
293                     switch (methodName) {
294                       case "add": // SDK 16
295                       case "addToDisplay": // SDK 17-29
296                       case "addToDisplayAsUser": // SDK 30+
297                         return windowSessionDelegate.getAddFlags();
298                       case "getInTouchMode":
299                         return windowSessionDelegate.getInTouchMode();
300                       case "performDrag":
301                         return windowSessionDelegate.performDrag(args);
302                       case "prepareDrag":
303                         return windowSessionDelegate.prepareDrag();
304                       case "setInTouchMode":
305                         windowSessionDelegate.setInTouchMode((boolean) args[0]);
306                         return null;
307                       case "setOnBackInvokedCallbackInfo":
308                         windowSessionDelegate.onBackInvokedCallbackInfo =
309                             (OnBackInvokedCallbackInfo) args[1];
310                         return null;
311                       case "reportSystemGestureExclusionChanged":
312                         windowSessionDelegate.systemGestureExclusionRects = (List<Rect>) args[1];
313                         return null;
314                       default:
315                         return ReflectionHelpers.defaultValueForType(
316                             method.getReturnType().getName());
317                     }
318                   });
319     }
320     return windowSession;
321   }
322 
323   @Implementation
peekWindowSession()324   protected static synchronized IWindowSession peekWindowSession() {
325     return windowSession;
326   }
327 
328   @Implementation
getWindowManagerService()329   public static Object getWindowManagerService() throws RemoteException {
330     IWindowManager service =
331         reflector(WindowManagerGlobalReflector.class).getWindowManagerService();
332     if (service == null) {
333       service = IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));
334       reflector(WindowManagerGlobalReflector.class).setWindowManagerService(service);
335     }
336     return service;
337   }
338 
339   @ForType(WindowManagerGlobal.class)
340   interface WindowManagerGlobalReflector {
341     @Accessor("sDefaultWindowManager")
342     @Static
setDefaultWindowManager(WindowManagerGlobal global)343     void setDefaultWindowManager(WindowManagerGlobal global);
344 
345     @Static
346     @Accessor("sWindowManagerService")
getWindowManagerService()347     IWindowManager getWindowManagerService();
348 
349     @Static
350     @Accessor("sWindowManagerService")
setWindowManagerService(IWindowManager service)351     void setWindowManagerService(IWindowManager service);
352 
353     @Accessor("mViews")
getWindowViews()354     List<View> getWindowViews();
355   }
356 
357   private static class WindowSessionDelegate {
358     // From WindowManagerGlobal (was WindowManagerImpl in JB).
359     static final int ADD_FLAG_IN_TOUCH_MODE = 0x1;
360     static final int ADD_FLAG_APP_VISIBLE = 0x2;
361 
362     // TODO: Default to touch mode always.
363     private boolean inTouchMode = useRealGraphics();
364     @Nullable protected ClipData lastDragClipData;
365     @Nullable private OnBackInvokedCallbackInfo onBackInvokedCallbackInfo;
366     @Nullable private List<Rect> systemGestureExclusionRects;
367     @Nullable private PredictiveBackGesture currentPredictiveBackGesture;
368 
getAddFlags()369     protected int getAddFlags() {
370       int res = 0;
371       // Temporarily enable this based on a system property to allow for test migration. This will
372       // eventually be updated to default and true and eventually removed, Robolectric's previous
373       // behavior of not marking windows as visible by default is a bug. This flag should only be
374       // used as a temporary toggle during migration.
375       if (useRealGraphics()
376           || "true".equals(System.getProperty("robolectric.areWindowsMarkedVisible", "false"))) {
377         res |= ADD_FLAG_APP_VISIBLE;
378       }
379       if (getInTouchMode()) {
380         res |= ADD_FLAG_IN_TOUCH_MODE;
381       }
382       return res;
383     }
384 
getInTouchMode()385     public boolean getInTouchMode() {
386       return inTouchMode;
387     }
388 
setInTouchMode(boolean inTouchMode)389     public void setInTouchMode(boolean inTouchMode) {
390       this.inTouchMode = inTouchMode;
391     }
392 
prepareDrag()393     public IBinder prepareDrag() {
394       return new Binder();
395     }
396 
performDrag(Object[] args)397     public Object performDrag(Object[] args) {
398       // extract the clipData param
399       for (int i = args.length - 1; i >= 0; i--) {
400         if (args[i] instanceof ClipData) {
401           lastDragClipData = (ClipData) args[i];
402           // In P (SDK 28), the return type changed from boolean to Binder.
403           return RuntimeEnvironment.getApiLevel() >= P ? new Binder() : true;
404         }
405       }
406       throw new AssertionError("Missing ClipData param");
407     }
408   }
409 
410   @ForType(BackMotionEvent.class)
411   interface BackMotionEventReflector {
412     @Constructor
newBackMotionEvent( float touchX, float touchY, float progress, float velocityX, float velocityY, int swipeEdge, RemoteAnimationTarget departingAnimationTarget)413     BackMotionEvent newBackMotionEvent(
414         float touchX,
415         float touchY,
416         float progress,
417         float velocityX,
418         float velocityY,
419         int swipeEdge,
420         RemoteAnimationTarget departingAnimationTarget);
421 
422     @Constructor
newBackMotionEventV( float touchX, float touchY, float progress, float velocityX, float velocityY, boolean triggerBack, int swipeEdge, RemoteAnimationTarget departingAnimationTarget)423     BackMotionEvent newBackMotionEventV(
424         float touchX,
425         float touchY,
426         float progress,
427         float velocityX,
428         float velocityY,
429         boolean triggerBack,
430         int swipeEdge,
431         RemoteAnimationTarget departingAnimationTarget);
432   }
433 
434   private static class BackMotionEvents {
BackMotionEvents()435     private BackMotionEvents() {}
436 
newBackMotionEvent( @ackEvent.SwipeEdge int edge, float touchX, float touchY, float progress)437     static BackMotionEvent newBackMotionEvent(
438         @BackEvent.SwipeEdge int edge, float touchX, float touchY, float progress) {
439       if (RuntimeEnvironment.getApiLevel() >= UPSIDE_DOWN_CAKE) {
440         try {
441           return reflector(BackMotionEventReflector.class)
442               .newBackMotionEventV(
443                   touchX,
444                   touchY,
445                   progress,
446                   0f, // velocity x
447                   0f, // velocity y
448                   Boolean.FALSE, // trigger back
449                   edge, // swipe edge
450                   null);
451         } catch (Throwable t) {
452           if (NoSuchMethodException.class.isInstance(t) || AssertionError.class.isInstance(t)) {
453             // fall through, assuming (perhaps falsely?) this exception is thrown by reflector(),
454             // and not the method reflected in to.
455           } else {
456             if (RuntimeException.class.isInstance(t)) {
457               throw (RuntimeException) t;
458             } else {
459               throw new RuntimeException(t);
460             }
461           }
462         }
463       }
464       return reflector(BackMotionEventReflector.class)
465           .newBackMotionEvent(
466               touchX, touchY, progress, 0f, // velocity x
467               0f, // velocity y
468               edge, // swipe edge
469               null);
470     }
471   }
472 }
473