1 package org.robolectric.shadows; 2 3 import static android.app.UiAutomation.ROTATION_FREEZE_0; 4 import static android.app.UiAutomation.ROTATION_FREEZE_180; 5 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; 6 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 7 import static android.os.Build.VERSION_CODES.TIRAMISU; 8 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 9 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 10 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; 11 import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 12 import static com.google.common.base.Preconditions.checkState; 13 import static java.util.Comparator.comparingInt; 14 import static java.util.stream.Collectors.toList; 15 import static java.util.stream.Collectors.toSet; 16 import static org.robolectric.Shadows.shadowOf; 17 18 import android.app.Activity; 19 import android.app.UiAutomation; 20 import android.content.ContentResolver; 21 import android.content.res.Configuration; 22 import android.content.res.Resources; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Paint; 26 import android.graphics.Point; 27 import android.os.Build; 28 import android.os.IBinder; 29 import android.os.Looper; 30 import android.provider.Settings; 31 import android.view.Display; 32 import android.view.InputEvent; 33 import android.view.KeyEvent; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.ViewRootImpl; 37 import android.view.WindowManager; 38 import android.view.WindowManagerGlobal; 39 import android.view.WindowManagerImpl; 40 import androidx.test.runner.lifecycle.ActivityLifecycleMonitor; 41 import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; 42 import androidx.test.runner.lifecycle.Stage; 43 import com.google.common.collect.ImmutableList; 44 import com.google.common.collect.ImmutableSet; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.List; 48 import java.util.Set; 49 import java.util.function.Predicate; 50 import org.robolectric.RuntimeEnvironment; 51 import org.robolectric.annotation.Implementation; 52 import org.robolectric.annotation.Implements; 53 import org.robolectric.util.ReflectionHelpers; 54 55 /** Shadow for {@link UiAutomation}. */ 56 @Implements(value = UiAutomation.class, minSdk = JELLY_BEAN_MR2) 57 public class ShadowUiAutomation { 58 59 private static final Predicate<Root> IS_FOCUSABLE = hasLayoutFlag(FLAG_NOT_FOCUSABLE).negate(); 60 private static final Predicate<Root> IS_TOUCHABLE = hasLayoutFlag(FLAG_NOT_TOUCHABLE).negate(); 61 private static final Predicate<Root> IS_TOUCH_MODAL = 62 IS_FOCUSABLE.and(hasLayoutFlag(FLAG_NOT_TOUCH_MODAL).negate()); 63 private static final Predicate<Root> WATCH_TOUCH_OUTSIDE = 64 IS_TOUCH_MODAL.negate().and(hasLayoutFlag(FLAG_WATCH_OUTSIDE_TOUCH)); 65 66 /** 67 * Sets the animation scale, see {@link UiAutomation#setAnimationScale(float)}. Provides backwards 68 * compatible access to SDKs < T. 69 */ 70 @SuppressWarnings("deprecation") setAnimationScaleCompat(float scale)71 public static void setAnimationScaleCompat(float scale) { 72 ContentResolver cr = RuntimeEnvironment.getApplication().getContentResolver(); 73 if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR1) { 74 Settings.Global.putFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, scale); 75 Settings.Global.putFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, scale); 76 Settings.Global.putFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, scale); 77 } else { 78 Settings.System.putFloat(cr, Settings.System.ANIMATOR_DURATION_SCALE, scale); 79 Settings.System.putFloat(cr, Settings.System.TRANSITION_ANIMATION_SCALE, scale); 80 Settings.System.putFloat(cr, Settings.System.WINDOW_ANIMATION_SCALE, scale); 81 } 82 } 83 84 @Implementation(minSdk = TIRAMISU) setAnimationScale(float scale)85 protected void setAnimationScale(float scale) { 86 setAnimationScaleCompat(scale); 87 } 88 89 @Implementation setRotation(int rotation)90 protected boolean setRotation(int rotation) { 91 if (rotation == UiAutomation.ROTATION_FREEZE_CURRENT 92 || rotation == UiAutomation.ROTATION_UNFREEZE) { 93 return true; 94 } 95 Display display = ShadowDisplay.getDefaultDisplay(); 96 int currentRotation = display.getRotation(); 97 boolean isRotated = 98 (rotation == ROTATION_FREEZE_0 || rotation == ROTATION_FREEZE_180) 99 != (currentRotation == ROTATION_FREEZE_0 || currentRotation == ROTATION_FREEZE_180); 100 shadowOf(display).setRotation(rotation); 101 if (isRotated) { 102 int currentOrientation = Resources.getSystem().getConfiguration().orientation; 103 String rotationQualifier = 104 "+" + (currentOrientation == Configuration.ORIENTATION_PORTRAIT ? "land" : "port"); 105 ShadowDisplayManager.changeDisplay(display.getDisplayId(), rotationQualifier); 106 RuntimeEnvironment.setQualifiers(rotationQualifier); 107 } 108 return true; 109 } 110 111 @Implementation throwIfNotConnectedLocked()112 protected void throwIfNotConnectedLocked() {} 113 114 @Implementation takeScreenshot()115 protected Bitmap takeScreenshot() { 116 if (!ShadowView.useRealGraphics()) { 117 return null; 118 } 119 Point displaySize = new Point(); 120 ShadowDisplay.getDefaultDisplay().getRealSize(displaySize); 121 Bitmap screenshot = Bitmap.createBitmap(displaySize.x, displaySize.y, Bitmap.Config.ARGB_8888); 122 Canvas screenshotCanvas = new Canvas(screenshot); 123 Paint paint = new Paint(); 124 for (Root root : getViewRoots().reverse()) { 125 View rootView = root.getRootView(); 126 if (rootView.getWidth() <= 0 || rootView.getHeight() <= 0) { 127 continue; 128 } 129 Bitmap window = 130 Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888); 131 Canvas windowCanvas = new Canvas(window); 132 rootView.draw(windowCanvas); 133 screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint); 134 } 135 return screenshot; 136 } 137 138 /** 139 * Injects a motion event into the appropriate window, see {@link 140 * UiAutomation#injectInputEvent(InputEvent, boolean)}. This can be used through the {@link 141 * UiAutomation} API, this method is provided for backwards compatibility with SDK < 18. 142 */ injectInputEvent(InputEvent event)143 public static boolean injectInputEvent(InputEvent event) { 144 checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!"); 145 if (event instanceof MotionEvent) { 146 return injectMotionEvent((MotionEvent) event); 147 } else if (event instanceof KeyEvent) { 148 return injectKeyEvent((KeyEvent) event); 149 } else { 150 throw new IllegalArgumentException("Unrecognized event type: " + event); 151 } 152 } 153 154 @Implementation injectInputEvent(InputEvent event, boolean sync)155 protected boolean injectInputEvent(InputEvent event, boolean sync) { 156 return injectInputEvent(event); 157 } 158 injectMotionEvent(MotionEvent event)159 private static boolean injectMotionEvent(MotionEvent event) { 160 // TODO(paulsowden): The real implementation will send a full event stream (a touch down 161 // followed by a series of moves, etc) to the same window/root even if the subsequent events 162 // leave the window bounds, and will split pointer down events based on the window flags. 163 // This will be necessary to support more sophisticated multi-window use cases. 164 165 List<Root> touchableRoots = getViewRoots().stream().filter(IS_TOUCHABLE).collect(toList()); 166 for (int i = 0; i < touchableRoots.size(); i++) { 167 Root root = touchableRoots.get(i); 168 if (i == touchableRoots.size() - 1 || root.isTouchModal() || root.isTouchInside(event)) { 169 event.offsetLocation(-root.params.x, -root.params.y); 170 root.getRootView().dispatchTouchEvent(event); 171 event.offsetLocation(root.params.x, root.params.y); 172 break; 173 } else if (event.getActionMasked() == MotionEvent.ACTION_DOWN && root.watchTouchOutside()) { 174 MotionEvent outsideEvent = MotionEvent.obtain(event); 175 outsideEvent.setAction(MotionEvent.ACTION_OUTSIDE); 176 outsideEvent.offsetLocation(-root.params.x, -root.params.y); 177 root.getRootView().dispatchTouchEvent(outsideEvent); 178 outsideEvent.recycle(); 179 } 180 } 181 return true; 182 } 183 injectKeyEvent(KeyEvent event)184 private static boolean injectKeyEvent(KeyEvent event) { 185 getViewRoots().stream() 186 .filter(IS_FOCUSABLE) 187 .findFirst() 188 .ifPresent(root -> root.getRootView().dispatchKeyEvent(event)); 189 return true; 190 } 191 getViewRoots()192 private static ImmutableList<Root> getViewRoots() { 193 List<ViewRootImpl> viewRootImpls = getViewRootImpls(); 194 List<WindowManager.LayoutParams> params = getRootLayoutParams(); 195 checkState( 196 params.size() == viewRootImpls.size(), 197 "number params is not consistent with number of view roots!"); 198 Set<IBinder> startedActivityTokens = getStartedActivityTokens(); 199 ArrayList<Root> roots = new ArrayList<>(); 200 for (int i = 0; i < viewRootImpls.size(); i++) { 201 Root root = new Root(viewRootImpls.get(i), params.get(i), i); 202 // TODO: Should we also filter out sub-windows of non-started application windows? 203 if (root.getType() != WindowManager.LayoutParams.TYPE_BASE_APPLICATION 204 || startedActivityTokens.contains(root.impl.getView().getApplicationWindowToken())) { 205 roots.add(root); 206 } 207 } 208 roots.sort( 209 comparingInt(Root::getType) 210 .reversed() 211 .thenComparing(comparingInt(Root::getIndex).reversed())); 212 return ImmutableList.copyOf(roots); 213 } 214 215 @SuppressWarnings("unchecked") getViewRootImpls()216 private static List<ViewRootImpl> getViewRootImpls() { 217 Object windowManager = getViewRootsContainer(); 218 Object viewRootsObj = ReflectionHelpers.getField(windowManager, "mRoots"); 219 Class<?> viewRootsClass = viewRootsObj.getClass(); 220 if (ViewRootImpl[].class.isAssignableFrom(viewRootsClass)) { 221 return Arrays.asList((ViewRootImpl[]) viewRootsObj); 222 } else if (List.class.isAssignableFrom(viewRootsClass)) { 223 return (List<ViewRootImpl>) viewRootsObj; 224 } else { 225 throw new IllegalStateException( 226 "WindowManager.mRoots is an unknown type " + viewRootsClass.getName()); 227 } 228 } 229 230 @SuppressWarnings("unchecked") getRootLayoutParams()231 private static List<WindowManager.LayoutParams> getRootLayoutParams() { 232 Object windowManager = getViewRootsContainer(); 233 Object paramsObj = ReflectionHelpers.getField(windowManager, "mParams"); 234 Class<?> paramsClass = paramsObj.getClass(); 235 if (WindowManager.LayoutParams[].class.isAssignableFrom(paramsClass)) { 236 return Arrays.asList((WindowManager.LayoutParams[]) paramsObj); 237 } else if (List.class.isAssignableFrom(paramsClass)) { 238 return (List<WindowManager.LayoutParams>) paramsObj; 239 } else { 240 throw new IllegalStateException( 241 "WindowManager.mParams is an unknown type " + paramsClass.getName()); 242 } 243 } 244 getViewRootsContainer()245 private static Object getViewRootsContainer() { 246 if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.JELLY_BEAN) { 247 return ReflectionHelpers.callStaticMethod(WindowManagerImpl.class, "getDefault"); 248 } else { 249 return WindowManagerGlobal.getInstance(); 250 } 251 } 252 getStartedActivityTokens()253 private static Set<IBinder> getStartedActivityTokens() { 254 ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance(); 255 return ImmutableSet.<Activity>builder() 256 .addAll(monitor.getActivitiesInStage(Stage.STARTED)) 257 .addAll(monitor.getActivitiesInStage(Stage.RESUMED)) 258 .build() 259 .stream() 260 .map(activity -> activity.getWindow().getDecorView().getApplicationWindowToken()) 261 .collect(toSet()); 262 } 263 hasLayoutFlag(int flag)264 private static Predicate<Root> hasLayoutFlag(int flag) { 265 return root -> (root.params.flags & flag) == flag; 266 } 267 268 private static final class Root { 269 final ViewRootImpl impl; 270 final WindowManager.LayoutParams params; 271 final int index; 272 Root(ViewRootImpl impl, WindowManager.LayoutParams params, int index)273 Root(ViewRootImpl impl, WindowManager.LayoutParams params, int index) { 274 this.impl = impl; 275 this.params = params; 276 this.index = index; 277 } 278 getIndex()279 int getIndex() { 280 return index; 281 } 282 getType()283 int getType() { 284 return params.type; 285 } 286 getRootView()287 View getRootView() { 288 return impl.getView(); 289 } 290 isTouchInside(MotionEvent event)291 boolean isTouchInside(MotionEvent event) { 292 int index = event.getActionIndex(); 293 return event.getX(index) >= params.x 294 && event.getX(index) <= params.x + impl.getView().getWidth() 295 && event.getY(index) >= params.y 296 && event.getY(index) <= params.y + impl.getView().getHeight(); 297 } 298 isTouchModal()299 boolean isTouchModal() { 300 return IS_TOUCH_MODAL.test(this); 301 } 302 watchTouchOutside()303 boolean watchTouchOutside() { 304 return WATCH_TOUCH_OUTSIDE.test(this); 305 } 306 } 307 } 308