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