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