blob: 9e51896d91bcfc5b375a79698fc97789aa756702 [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityService.GestureResultCallback;
import android.accessibilityservice.AccessibilityService.ScreenshotErrorCode;
import android.accessibilityservice.AccessibilityService.ScreenshotResult;
import android.accessibilityservice.AccessibilityService.TakeScreenshotCallback;
import android.accessibilityservice.GestureDescription;
import android.graphics.ColorSpace;
import android.graphics.ColorSpace.Named;
import android.hardware.HardwareBuffer;
import android.os.Handler;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.Display;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import com.google.common.collect.ArrayListMultimap;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
/**
* Shadow of AccessibilityService that tracks global actions and provides a mechanism to simulate
* the window list.
*/
@Implements(AccessibilityService.class)
public class ShadowAccessibilityService extends ShadowService {
private final List<Integer> globalActionsPerformed = new ArrayList<>();
private List<AccessibilityNodeInfo.AccessibilityAction> systemActions;
private final ArrayListMultimap<Integer, AccessibilityWindowInfo> windows =
ArrayListMultimap.create();
private final List<GestureDispatch> gesturesDispatched = new ArrayList<>();
private boolean canDispatchGestures = true;
@ScreenshotErrorCode
private int takeScreenshotErrorCode = AccessibilityService.ERROR_TAKE_SCREENSHOT_INTERNAL_ERROR;
private boolean isScreenshotError = false;
@Implementation
protected final boolean performGlobalAction(int action) {
globalActionsPerformed.add(action);
return true;
}
public List<Integer> getGlobalActionsPerformed() {
return globalActionsPerformed;
}
@Implementation(minSdk = S)
protected final List<AccessibilityNodeInfo.AccessibilityAction> getSystemActions() {
return systemActions;
}
public final void setSystemActions(
List<AccessibilityNodeInfo.AccessibilityAction> systemActions) {
this.systemActions = systemActions;
}
/**
* Returns a representation of interactive windows shown on the device's default display. Mirrors
* the values provided to {@link #setWindows(List<AccessibilityWindowInfo>)}. Returns an empty
* list if not set.
*/
@Implementation(minSdk = LOLLIPOP)
protected List<AccessibilityWindowInfo> getWindows() {
List<AccessibilityWindowInfo> windowInfos = windows.get(Display.DEFAULT_DISPLAY);
if (windowInfos != null) {
return new ArrayList<>(windowInfos);
} else {
return new ArrayList<>();
}
}
/**
* Returns a representation of interactive windows shown on the device's all displays. An empty
* list will be returned for default display and {@code null} will be return for other displays if
* they are not set by {@link #setWindowsOnDisplay(int, List)}.
*/
@Implementation(minSdk = R)
protected SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays() {
return cloneWindowOnAllDisplays();
}
private SparseArray<List<AccessibilityWindowInfo>> cloneWindowOnAllDisplays() {
SparseArray<List<AccessibilityWindowInfo>> anotherArray = new SparseArray<>();
for (int displayId : windows.keySet()) {
anotherArray.put(displayId, windows.get(displayId));
}
if (anotherArray.get(Display.DEFAULT_DISPLAY) == null) {
anotherArray.put(Display.DEFAULT_DISPLAY, new ArrayList<>());
}
return anotherArray;
}
@Implementation(minSdk = N)
protected boolean dispatchGesture(
GestureDescription gesture, GestureResultCallback callback, Handler handler) {
if (canDispatchGestures) {
gesturesDispatched.add(new GestureDispatch(gesture, callback));
}
return canDispatchGestures;
}
@Implementation(minSdk = R)
protected void takeScreenshot(
int displayId, Executor executor, AccessibilityService.TakeScreenshotCallback callback) {
executor.execute(
() -> {
if (isScreenshotError) {
callback.onFailure(takeScreenshotErrorCode);
return;
}
HardwareBuffer hardwareBuffer =
HardwareBuffer.create(1, 1, HardwareBuffer.RGBA_8888, 1, 0L);
ColorSpace colorSpace = ColorSpace.get(Named.SRGB);
long timestamp = SystemClock.elapsedRealtimeNanos();
ScreenshotResult screenshotResult =
ReflectionHelpers.callConstructor(
ScreenshotResult.class,
new ClassParameter<>(HardwareBuffer.class, hardwareBuffer),
new ClassParameter<>(ColorSpace.class, colorSpace),
new ClassParameter<>(long.class, timestamp));
callback.onSuccess(screenshotResult);
});
}
/**
* Sets {@link AccessibilityService#takeScreenshot(int, Executor, TakeScreenshotCallback)} to
* start returning the given {@code errorCode}.
*
* @see #unsetTakeScreenshotErrorCode() to unset the error condition.
*/
public void setTakeScreenshotErrorCode(@ScreenshotErrorCode int errorCode) {
this.isScreenshotError = true;
this.takeScreenshotErrorCode = errorCode;
}
/**
* Sets {@link AccessibilityService#takeScreenshot(int, Executor, TakeScreenshotCallback)} to
* start returning successful results again.
*
* @see #setTakeScreenshotErrorCode(int) to set an error condition instead.
*/
public void unsetTakeScreenshotErrorCode() {
this.isScreenshotError = false;
}
/**
* Sets the list of interactive windows shown on the device's default display as reported by
* {@link #getWindows()}
*/
public void setWindows(List<AccessibilityWindowInfo> windowList) {
setWindowsOnDisplay(Display.DEFAULT_DISPLAY, windowList);
}
/**
* Sets the list of interactive windows shown on the device's {@code displayId} display. If the
* {@code windowList} is null, we will remove the list with given {@code displayId} display as
* reported by {@link #getWindowsOnAllDisplays()}.
*/
public void setWindowsOnDisplay(
int displayId, @Nullable List<AccessibilityWindowInfo> windowList) {
windows.removeAll(displayId);
if (windowList != null) {
windows.putAll(displayId, windowList);
}
}
/**
* Returns a list of gestures that have been dispatched.
*
* Gestures are dispatched by calling {@link AccessibilityService#dispatchGesture}.
*/
public List<GestureDispatch> getGesturesDispatched() {
return gesturesDispatched;
}
/**
* Sets whether the service is currently able to dispatch gestures.
*
* If {@code false}, {@link AccessibilityService#dispatchGesture} will return {@code false}.
*/
public void setCanDispatchGestures(boolean canDispatchGestures) {
this.canDispatchGestures = canDispatchGestures;
}
/**
* Represents a gesture that has been dispatched through the accessibility service.
*
* Gestures are dispatched by calling {@link AccessibilityService#dispatchGesture}.
*/
public static final class GestureDispatch {
private final GestureDescription description;
private final GestureResultCallback callback;
public GestureDispatch(GestureDescription description, GestureResultCallback callback) {
this.description = description;
this.callback = callback;
}
/** The description of the gesture to be dispatched. Includes timestamps and the path. */
public GestureDescription description() {
return description;
}
/**
* The callback that is to be invoked once the gesture has finished dispatching.
*
* The shadow itself does not invoke this callback. You must manually invoke it to run it.
*/
public GestureResultCallback callback() {
return callback;
}
}
}