| 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; |
| } |
| } |
| } |