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