org.robolectric.shadows.ShadowUiAutomation Maven / Gradle / Ivy
package org.robolectric.shadows;
import static android.app.UiAutomation.ROTATION_FREEZE_0;
import static android.app.UiAutomation.ROTATION_FREEZE_180;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.robolectric.Shadows.shadowOf;
import android.app.Activity;
import android.app.UiAutomation;
import android.content.ContentResolver;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.os.Build;
import android.os.IBinder;
import android.os.Looper;
import android.provider.Settings;
import android.view.Display;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import android.view.WindowManagerImpl;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
/** Shadow for {@link UiAutomation}. */
@Implements(value = UiAutomation.class, minSdk = JELLY_BEAN_MR2)
public class ShadowUiAutomation {
private static final Predicate IS_FOCUSABLE = hasLayoutFlag(FLAG_NOT_FOCUSABLE).negate();
private static final Predicate IS_TOUCHABLE = hasLayoutFlag(FLAG_NOT_TOUCHABLE).negate();
private static final Predicate IS_TOUCH_MODAL =
IS_FOCUSABLE.and(hasLayoutFlag(FLAG_NOT_TOUCH_MODAL).negate());
private static final Predicate WATCH_TOUCH_OUTSIDE =
IS_TOUCH_MODAL.negate().and(hasLayoutFlag(FLAG_WATCH_OUTSIDE_TOUCH));
/**
* Sets the animation scale, see {@link UiAutomation#setAnimationScale(float)}. Provides backwards
* compatible access to SDKs < T.
*/
@SuppressWarnings("deprecation")
public static void setAnimationScaleCompat(float scale) {
ContentResolver cr = RuntimeEnvironment.getApplication().getContentResolver();
if (RuntimeEnvironment.getApiLevel() >= JELLY_BEAN_MR1) {
Settings.Global.putFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, scale);
Settings.Global.putFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, scale);
Settings.Global.putFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, scale);
} else {
Settings.System.putFloat(cr, Settings.System.ANIMATOR_DURATION_SCALE, scale);
Settings.System.putFloat(cr, Settings.System.TRANSITION_ANIMATION_SCALE, scale);
Settings.System.putFloat(cr, Settings.System.WINDOW_ANIMATION_SCALE, scale);
}
}
@Implementation(minSdk = TIRAMISU)
protected void setAnimationScale(float scale) {
setAnimationScaleCompat(scale);
}
@Implementation
protected boolean setRotation(int rotation) {
if (rotation == UiAutomation.ROTATION_FREEZE_CURRENT
|| rotation == UiAutomation.ROTATION_UNFREEZE) {
return true;
}
Display display = ShadowDisplay.getDefaultDisplay();
int currentRotation = display.getRotation();
boolean isRotated =
(rotation == ROTATION_FREEZE_0 || rotation == ROTATION_FREEZE_180)
!= (currentRotation == ROTATION_FREEZE_0 || currentRotation == ROTATION_FREEZE_180);
shadowOf(display).setRotation(rotation);
if (isRotated) {
int currentOrientation = Resources.getSystem().getConfiguration().orientation;
String rotationQualifier =
"+" + (currentOrientation == Configuration.ORIENTATION_PORTRAIT ? "land" : "port");
ShadowDisplayManager.changeDisplay(display.getDisplayId(), rotationQualifier);
RuntimeEnvironment.setQualifiers(rotationQualifier);
}
return true;
}
@Implementation
protected void throwIfNotConnectedLocked() {}
@Implementation
protected Bitmap takeScreenshot() {
if (!ShadowView.useRealGraphics()) {
return null;
}
Point displaySize = new Point();
ShadowDisplay.getDefaultDisplay().getRealSize(displaySize);
Bitmap screenshot = Bitmap.createBitmap(displaySize.x, displaySize.y, Bitmap.Config.ARGB_8888);
Canvas screenshotCanvas = new Canvas(screenshot);
Paint paint = new Paint();
for (Root root : getViewRoots().reverse()) {
View rootView = root.getRootView();
if (rootView.getWidth() <= 0 || rootView.getHeight() <= 0) {
continue;
}
Bitmap window =
Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas windowCanvas = new Canvas(window);
rootView.draw(windowCanvas);
screenshotCanvas.drawBitmap(window, root.params.x, root.params.y, paint);
}
return screenshot;
}
/**
* Injects a motion event into the appropriate window, see {@link
* UiAutomation#injectInputEvent(InputEvent, boolean)}. This can be used through the {@link
* UiAutomation} API, this method is provided for backwards compatibility with SDK < 18.
*/
public static boolean injectInputEvent(InputEvent event) {
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
if (event instanceof MotionEvent) {
return injectMotionEvent((MotionEvent) event);
} else if (event instanceof KeyEvent) {
return injectKeyEvent((KeyEvent) event);
} else {
throw new IllegalArgumentException("Unrecognized event type: " + event);
}
}
@Implementation
protected boolean injectInputEvent(InputEvent event, boolean sync) {
return injectInputEvent(event);
}
private static boolean injectMotionEvent(MotionEvent event) {
// TODO(paulsowden): The real implementation will send a full event stream (a touch down
// followed by a series of moves, etc) to the same window/root even if the subsequent events
// leave the window bounds, and will split pointer down events based on the window flags.
// This will be necessary to support more sophisticated multi-window use cases.
List touchableRoots = getViewRoots().stream().filter(IS_TOUCHABLE).collect(toList());
for (int i = 0; i < touchableRoots.size(); i++) {
Root root = touchableRoots.get(i);
if (i == touchableRoots.size() - 1 || root.isTouchModal() || root.isTouchInside(event)) {
event.offsetLocation(-root.params.x, -root.params.y);
root.getRootView().dispatchTouchEvent(event);
event.offsetLocation(root.params.x, root.params.y);
break;
} else if (event.getActionMasked() == MotionEvent.ACTION_DOWN && root.watchTouchOutside()) {
MotionEvent outsideEvent = MotionEvent.obtain(event);
outsideEvent.setAction(MotionEvent.ACTION_OUTSIDE);
outsideEvent.offsetLocation(-root.params.x, -root.params.y);
root.getRootView().dispatchTouchEvent(outsideEvent);
outsideEvent.recycle();
}
}
return true;
}
private static boolean injectKeyEvent(KeyEvent event) {
getViewRoots().stream()
.filter(IS_FOCUSABLE)
.findFirst()
.ifPresent(root -> root.getRootView().dispatchKeyEvent(event));
return true;
}
private static ImmutableList getViewRoots() {
List viewRootImpls = getViewRootImpls();
List params = getRootLayoutParams();
checkState(
params.size() == viewRootImpls.size(),
"number params is not consistent with number of view roots!");
Set startedActivityTokens = getStartedActivityTokens();
ArrayList roots = new ArrayList<>();
for (int i = 0; i < viewRootImpls.size(); i++) {
Root root = new Root(viewRootImpls.get(i), params.get(i), i);
// TODO: Should we also filter out sub-windows of non-started application windows?
if (root.getType() != WindowManager.LayoutParams.TYPE_BASE_APPLICATION
|| startedActivityTokens.contains(root.impl.getView().getApplicationWindowToken())) {
roots.add(root);
}
}
roots.sort(
comparingInt(Root::getType)
.reversed()
.thenComparing(comparingInt(Root::getIndex).reversed()));
return ImmutableList.copyOf(roots);
}
@SuppressWarnings("unchecked")
private static List getViewRootImpls() {
Object windowManager = getViewRootsContainer();
Object viewRootsObj = ReflectionHelpers.getField(windowManager, "mRoots");
Class> viewRootsClass = viewRootsObj.getClass();
if (ViewRootImpl[].class.isAssignableFrom(viewRootsClass)) {
return Arrays.asList((ViewRootImpl[]) viewRootsObj);
} else if (List.class.isAssignableFrom(viewRootsClass)) {
return (List) viewRootsObj;
} else {
throw new IllegalStateException(
"WindowManager.mRoots is an unknown type " + viewRootsClass.getName());
}
}
@SuppressWarnings("unchecked")
private static List getRootLayoutParams() {
Object windowManager = getViewRootsContainer();
Object paramsObj = ReflectionHelpers.getField(windowManager, "mParams");
Class> paramsClass = paramsObj.getClass();
if (WindowManager.LayoutParams[].class.isAssignableFrom(paramsClass)) {
return Arrays.asList((WindowManager.LayoutParams[]) paramsObj);
} else if (List.class.isAssignableFrom(paramsClass)) {
return (List) paramsObj;
} else {
throw new IllegalStateException(
"WindowManager.mParams is an unknown type " + paramsClass.getName());
}
}
private static Object getViewRootsContainer() {
if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.JELLY_BEAN) {
return ReflectionHelpers.callStaticMethod(WindowManagerImpl.class, "getDefault");
} else {
return WindowManagerGlobal.getInstance();
}
}
private static Set getStartedActivityTokens() {
ActivityLifecycleMonitor monitor = ActivityLifecycleMonitorRegistry.getInstance();
return ImmutableSet.builder()
.addAll(monitor.getActivitiesInStage(Stage.STARTED))
.addAll(monitor.getActivitiesInStage(Stage.RESUMED))
.build()
.stream()
.map(activity -> activity.getWindow().getDecorView().getApplicationWindowToken())
.collect(toSet());
}
private static Predicate hasLayoutFlag(int flag) {
return root -> (root.params.flags & flag) == flag;
}
private static final class Root {
final ViewRootImpl impl;
final WindowManager.LayoutParams params;
final int index;
Root(ViewRootImpl impl, WindowManager.LayoutParams params, int index) {
this.impl = impl;
this.params = params;
this.index = index;
}
int getIndex() {
return index;
}
int getType() {
return params.type;
}
View getRootView() {
return impl.getView();
}
boolean isTouchInside(MotionEvent event) {
int index = event.getActionIndex();
return event.getX(index) >= params.x
&& event.getX(index) <= params.x + impl.getView().getWidth()
&& event.getY(index) >= params.y
&& event.getY(index) <= params.y + impl.getView().getHeight();
}
boolean isTouchModal() {
return IS_TOUCH_MODAL.test(this);
}
boolean watchTouchOutside() {
return WATCH_TOUCH_OUTSIDE.test(this);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy