diff --git a/app/build.gradle b/app/build.gradle index fed25a391..51c18925b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,11 +14,11 @@ apply plugin: 'com.android.application' apply plugin: 'de.mobilej.unmock' android { - compileSdk 33 + compileSdk 34 defaultConfig { applicationId 'io.appium.uiautomator2' minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 151 archivesBaseName = 'appium-uiautomator2' /** @@ -89,7 +89,7 @@ unMock { dependencies { // https://download.eclipse.org/oomph/archive/reports/download.eclipse.org/releases/2021-09/index/org.eclipse.wst.xml.xpath2.processor_2.1.101.v201903222120.html implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.test.uiautomator:uiautomator:2.2.0' + implementation 'androidx.test.uiautomator:uiautomator:2.3.0-beta01' implementation 'androidx.test:core:1.5.0' implementation 'androidx.test:runner:1.5.2' implementation 'com.google.code.gson:gson:2.10.1' diff --git a/app/src/main/java/io/appium/uiautomator2/core/AxNodeInfoHelper.java b/app/src/main/java/io/appium/uiautomator2/core/AxNodeInfoHelper.java index 66326d97d..e695cc0e2 100644 --- a/app/src/main/java/io/appium/uiautomator2/core/AxNodeInfoHelper.java +++ b/app/src/main/java/io/appium/uiautomator2/core/AxNodeInfoHelper.java @@ -23,25 +23,22 @@ import android.os.Build; import android.os.Bundle; import android.util.Pair; +import android.view.Display; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityWindowInfo; import androidx.annotation.Nullable; import androidx.test.uiautomator.Direction; -import androidx.test.uiautomator.UiDevice; - -import java.util.HashSet; -import java.util.Set; import io.appium.uiautomator2.common.exceptions.InvalidElementStateException; import io.appium.uiautomator2.model.internal.CustomUiDevice; +import io.appium.uiautomator2.model.internal.GestureController; import io.appium.uiautomator2.model.settings.Settings; import io.appium.uiautomator2.model.settings.SimpleBoundsCalculation; import io.appium.uiautomator2.model.settings.SnapshotMaxDepth; import io.appium.uiautomator2.utils.Logger; -import static io.appium.uiautomator2.utils.Device.getUiDevice; import static io.appium.uiautomator2.utils.ReflectionUtils.getField; import static io.appium.uiautomator2.utils.StringHelpers.charSequenceToNullableString; import static io.appium.uiautomator2.utils.StringHelpers.charSequenceToString; @@ -53,6 +50,13 @@ public class AxNodeInfoHelper { private static final long UNDEFINED_NODE_ID = (((long) Integer.MAX_VALUE) << 32) | Integer.MAX_VALUE; private static final int UNDEFINED_WINDOW_ID = -1; + private static final float DEFAULT_GESTURE_MARGIN_PERCENT = 0.1f; + private static final Margins mMargins = new PercentMargins( + DEFAULT_GESTURE_MARGIN_PERCENT, + DEFAULT_GESTURE_MARGIN_PERCENT, + DEFAULT_GESTURE_MARGIN_PERCENT, + DEFAULT_GESTURE_MARGIN_PERCENT + ); @Nullable public static String toUuid(AccessibilityNodeInfo info) { @@ -114,26 +118,17 @@ private static Point getCenterPoint(Rect bounds) { private static Rect getBoundsForGestures(AccessibilityNodeInfo node) { Rect bounds = getBounds(node); - // The default margin values are copied from UiObject2 class: - // private int mMarginLeft = 5; - // private int mMarginTop = 5; - // private int mMarginRight = 5; - // private int mMarginBottom = 5; - bounds.left = bounds.left + 5; - bounds.top = bounds.top + 5; - bounds.right = bounds.right - 5; - bounds.bottom = bounds.bottom - 5; - return bounds; + return mMargins.apply(bounds); } public static void click(AccessibilityNodeInfo node) { Rect bounds = getBounds(node); - CustomUiDevice.getInstance().getGestureController().click(getCenterPoint(bounds)); + makeGestureController(node).click(getCenterPoint(bounds)); } public static void doubleClick(AccessibilityNodeInfo node) { Rect bounds = getBounds(node); - CustomUiDevice.getInstance().getGestureController().doubleClick(getCenterPoint(bounds)); + makeGestureController(node).doubleClick(getCenterPoint(bounds)); } public static void longClick(AccessibilityNodeInfo node) { @@ -142,7 +137,7 @@ public static void longClick(AccessibilityNodeInfo node) { public static void longClick(AccessibilityNodeInfo node, @Nullable Long durationMs) { Rect bounds = getBounds(node); - CustomUiDevice.getInstance().getGestureController().longClick(getCenterPoint(bounds), durationMs); + makeGestureController(node).longClick(getCenterPoint(bounds), durationMs); } public static void drag(AccessibilityNodeInfo node, Point end) { @@ -151,7 +146,7 @@ public static void drag(AccessibilityNodeInfo node, Point end) { public static void drag(AccessibilityNodeInfo node, Point end, @Nullable Integer speed) { Rect bounds = getBounds(node); - CustomUiDevice.getInstance().getGestureController().drag(getCenterPoint(bounds), end, speed); + makeGestureController(node).drag(getCenterPoint(bounds), end, speed); } public static void pinchClose(AccessibilityNodeInfo node, float percent) { @@ -160,7 +155,21 @@ public static void pinchClose(AccessibilityNodeInfo node, float percent) { public static void pinchClose(AccessibilityNodeInfo node, float percent, @Nullable Integer speed) { Rect bounds = getBoundsForGestures(node); - CustomUiDevice.getInstance().getGestureController().pinchClose(bounds, percent, speed); + makeGestureController(node).pinchClose(bounds, percent, speed); + } + + private static GestureController makeGestureController(AccessibilityNodeInfo node) { + return CustomUiDevice.getInstance().getGestureController(getAxNodeDisplayId(node)); + } + + public static int getAxNodeDisplayId(AccessibilityNodeInfo node) { + if (node != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + AccessibilityWindowInfo window = node.getWindow(); + if (window != null) { + return window.getDisplayId(); + } + } + return Display.DEFAULT_DISPLAY; } public static void pinchOpen(AccessibilityNodeInfo node, float percent) { @@ -169,54 +178,101 @@ public static void pinchOpen(AccessibilityNodeInfo node, float percent) { public static void pinchOpen(AccessibilityNodeInfo node, float percent, @Nullable Integer speed) { Rect bounds = getBoundsForGestures(node); - CustomUiDevice.getInstance().getGestureController().pinchOpen(bounds, percent, speed); + makeGestureController(node).pinchOpen(bounds, percent, speed); } public static void swipe(AccessibilityNodeInfo node, Direction direction, float percent) { swipe(node, direction, percent, null); } - public static void swipe(AccessibilityNodeInfo node, Direction direction, float percent, @Nullable Integer speed) { + public static void swipe( + AccessibilityNodeInfo node, Direction direction, float percent, @Nullable Integer speed + ) { Rect bounds = getBoundsForGestures(node); - CustomUiDevice.getInstance().getGestureController().swipe(bounds, direction, percent, speed); + makeGestureController(node).swipe(bounds, direction, percent, speed); } public static boolean scroll(AccessibilityNodeInfo node, Direction direction, float percent) { return scroll(node, direction, percent, null); } - public static boolean scroll(AccessibilityNodeInfo node, Direction direction, float percent, @Nullable Integer speed) { + public static boolean scroll( + AccessibilityNodeInfo node, Direction direction, float percent, @Nullable Integer speed + ) { Rect bounds = getBoundsForGestures(node); - return CustomUiDevice.getInstance().getGestureController().scroll(bounds, direction, percent, speed); + return makeGestureController(node).scroll(bounds, direction, percent, speed); } public static boolean fling(AccessibilityNodeInfo node, Direction direction) { return fling(node, direction, null); } - public static boolean fling(AccessibilityNodeInfo node, Direction direction, @Nullable Integer speed) { + public static boolean fling( + AccessibilityNodeInfo node, Direction direction, @Nullable Integer speed + ) { Rect bounds = getBoundsForGestures(node); - return CustomUiDevice.getInstance().getGestureController().fling(bounds, direction, speed); + return makeGestureController(node).fling(bounds, direction, speed); } - /** - * Returns the node's bounds clipped to the size of the display - * - * @return Empty Rect if node is null, else a Rect containing visible bounds - */ public static Rect getBounds(@Nullable AccessibilityNodeInfo node) { - Rect rect = new Rect(); + int displayId = AxNodeInfoHelper.getAxNodeDisplayId(node); + final boolean isDisplayAccessible = CustomUiDevice.getInstance().getDisplayById(displayId) != null; + Rect screen = null; + if (isDisplayAccessible) { + Point displaySize = CustomUiDevice.getInstance().getDisplaySize(displayId); + screen = new Rect(0, 0, displaySize.x, displaySize.y); + } if (node == null) { - return rect; + return screen == null ? new Rect() : screen; } - if (Settings.get(SimpleBoundsCalculation.class).getValue()) { - node.getBoundsInScreen(rect); - return rect; + return getVisibleBoundsInScreen( + node, + screen, + Boolean.FALSE.equals(Settings.get(SimpleBoundsCalculation.class).getValue()), + 0 + ); + } + + @SuppressLint("CheckResult") + private static Rect getVisibleBoundsInScreen( + AccessibilityNodeInfo node, Rect displayRect, boolean trimScrollableParent, int depth + ) { + Rect nodeRect = new Rect(); + node.getBoundsInScreen(nodeRect); + + if (displayRect == null) { + displayRect = new Rect(); + } + nodeRect.intersect(displayRect); + + // Trim any portion of the bounds that are outside the window + Rect bounds = new Rect(); + AccessibilityWindowInfo window = node.getWindow(); + if (window != null) { + window.getBoundsInScreen(bounds); + nodeRect.intersect(bounds); } - UiDevice uiDevice = getUiDevice(); - Rect screenRect = new Rect(0, 0, uiDevice.getDisplayWidth(), uiDevice.getDisplayHeight()); - return getBounds(node, screenRect, 0); + // Trim the bounds into any scrollable ancestor, if required. + if (trimScrollableParent) { + for (AccessibilityNodeInfo ancestor = node.getParent(); + ancestor != null; + ancestor = ancestor.getParent() + ) { + if (ancestor.isScrollable()) { + if (depth >= Settings.get(SnapshotMaxDepth.class).getValue()) { + break; + } + Rect ancestorRect = getVisibleBoundsInScreen( + ancestor, displayRect, true, depth + 1 + ); + nodeRect.intersect(ancestorRect); + break; + } + } + } + + return nodeRect; } public static int calculateIndex(AccessibilityNodeInfo node) { @@ -232,55 +288,6 @@ public static int calculateIndex(AccessibilityNodeInfo node) { return 0; } - /** - * Returns the node's bounds clipped to the size of the display, limited by the SnapshotMaxDepth - * The implementation is borrowed from `getVisibleBounds` method of `UiObject2` class - * - * @return Empty rect if node is null, else a Rect containing visible bounds - */ - @SuppressLint("CheckResult") - private static Rect getBounds(@Nullable AccessibilityNodeInfo node, Rect displayRect, int depth) { - Rect ret = new Rect(); - if (node == null) { - return ret; - } - - // Get the object bounds in screen coordinates - node.getBoundsInScreen(ret); - - // Trim any portion of the bounds that are not on the screen - ret.intersect(displayRect); - - // Trim any portion of the bounds that are outside the window - Rect window = new Rect(); - AccessibilityWindowInfo nodeWindow = node.getWindow(); - if (nodeWindow != null) { - nodeWindow.getBoundsInScreen(window); - ret.intersect(window); - } - - // Find the visible bounds of our first scrollable ancestor - int currentDepth = depth; - Set ancestors = new HashSet<>(); - AccessibilityNodeInfo ancestor = node.getParent(); - // An erroneous situation is possible where node parent equals to the node itself - while (++currentDepth < Settings.get(SnapshotMaxDepth.class).getValue() - && ancestor != null && !ancestors.contains(ancestor)) { - // If this ancestor is scrollable - if (ancestor.isScrollable()) { - // Trim any portion of the bounds that are hidden by the non-visible portion of our - // ancestor - Rect ancestorRect = getBounds(ancestor, displayRect, currentDepth); - ret.intersect(ancestorRect); - return ret; - } - ancestors.add(ancestor); - ancestor = ancestor.getParent(); - } - - return ret; - } - /** * Perform accessibility action ACTION_SET_PROGRESS on the node * @@ -330,4 +337,26 @@ public static String truncateTextToMaxLength(final AccessibilityNodeInfo node, f } return text; } + + private interface Margins { + Rect apply(Rect bounds); + } + + private static class PercentMargins implements Margins { + float mLeft, mTop, mRight, mBottom; + PercentMargins(float left, float top, float right, float bottom) { + mLeft = left; + mTop = top; + mRight = right; + mBottom = bottom; + } + + @Override + public Rect apply(Rect bounds) { + return new Rect(bounds.left + (int) (bounds.width() * mLeft), + bounds.top + (int) (bounds.height() * mTop), + bounds.right - (int) (bounds.width() * mRight), + bounds.bottom - (int) (bounds.height() * mBottom)); + } + } } diff --git a/app/src/main/java/io/appium/uiautomator2/handler/gestures/Drag.java b/app/src/main/java/io/appium/uiautomator2/handler/gestures/Drag.java index 0d9892eeb..f047c9bcc 100644 --- a/app/src/main/java/io/appium/uiautomator2/handler/gestures/Drag.java +++ b/app/src/main/java/io/appium/uiautomator2/handler/gestures/Drag.java @@ -56,8 +56,9 @@ protected AppiumResponse safeHandle(IHttpRequest request) { Rect bounds = element.getBounds(); Point start = new Point(bounds.left + dragModel.start.x.intValue(), bounds.top + dragModel.start.y.intValue()); - CustomUiDevice.getInstance().getGestureController().drag(start, dragModel.end.toNativePoint(), - dragModel.speed); + CustomUiDevice.getInstance().getGestureController(element).drag( + start, dragModel.end.toNativePoint(), dragModel.speed + ); } } diff --git a/app/src/main/java/io/appium/uiautomator2/model/AndroidElement.java b/app/src/main/java/io/appium/uiautomator2/model/AndroidElement.java index a3c7d53c7..6e654876b 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/AndroidElement.java +++ b/app/src/main/java/io/appium/uiautomator2/model/AndroidElement.java @@ -30,6 +30,8 @@ public interface AndroidElement { @Nullable String getContextId(); + int getDisplayId(); + boolean isSingleMatch(); void clear() throws UiObjectNotFoundException; diff --git a/app/src/main/java/io/appium/uiautomator2/model/UiObject2Element.java b/app/src/main/java/io/appium/uiautomator2/model/UiObject2Element.java index 004a6ee67..e7bc15716 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/UiObject2Element.java +++ b/app/src/main/java/io/appium/uiautomator2/model/UiObject2Element.java @@ -161,6 +161,11 @@ public String getAttribute(String attr) throws UiObjectNotFoundException { return (result instanceof String) ? (String) result : String.valueOf(result); } + @Override + public int getDisplayId() { + return element.getDisplayId(); + } + @Override public void clear() { element.clear(); diff --git a/app/src/main/java/io/appium/uiautomator2/model/UiObjectElement.java b/app/src/main/java/io/appium/uiautomator2/model/UiObjectElement.java index 510bccda3..2da9e5504 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/UiObjectElement.java +++ b/app/src/main/java/io/appium/uiautomator2/model/UiObjectElement.java @@ -39,6 +39,7 @@ import io.appium.uiautomator2.utils.PositionHelper; import static io.appium.uiautomator2.core.AxNodeInfoExtractor.toAxNodeInfo; +import static io.appium.uiautomator2.core.AxNodeInfoHelper.getAxNodeDisplayId; import static io.appium.uiautomator2.model.AccessibleUiObject.toAccessibleUiObject; import static io.appium.uiautomator2.model.AccessibleUiObject.toAccessibleUiObjects; import static io.appium.uiautomator2.utils.ElementHelpers.generateNoAttributeException; @@ -152,6 +153,11 @@ public String getAttribute(String attr) throws UiObjectNotFoundException { return (result instanceof String) ? (String) result : String.valueOf(result); } + @Override + public int getDisplayId() { + return getAxNodeDisplayId(toAxNodeInfo(element)); + } + @Override public void clear() throws UiObjectNotFoundException { element.setText(""); diff --git a/app/src/main/java/io/appium/uiautomator2/model/internal/CustomUiDevice.java b/app/src/main/java/io/appium/uiautomator2/model/internal/CustomUiDevice.java index a16ad99ed..4707644fe 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/internal/CustomUiDevice.java +++ b/app/src/main/java/io/appium/uiautomator2/model/internal/CustomUiDevice.java @@ -18,9 +18,13 @@ import android.app.Instrumentation; import android.app.UiAutomation; +import android.graphics.Point; import android.os.Build; import android.os.SystemClock; +import android.util.SparseArray; +import android.view.Display; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -40,6 +44,7 @@ import io.appium.uiautomator2.common.exceptions.InvalidSelectorException; import io.appium.uiautomator2.common.exceptions.UiAutomator2Exception; import io.appium.uiautomator2.model.AccessibleUiObject; +import io.appium.uiautomator2.model.AndroidElement; import io.appium.uiautomator2.model.ScreenRotation; import io.appium.uiautomator2.utils.Device; import io.appium.uiautomator2.utils.Logger; @@ -65,15 +70,24 @@ public class CustomUiDevice { private final Class ByMatcherClass; private final Constructor uiObject2Constructor; private final Instrumentation mInstrumentation; - private GestureController gestureController; - + private final Object nativeGestureController; private CustomUiDevice() { this.mInstrumentation = (Instrumentation) getField(UiDevice.class, FIELD_M_INSTRUMENTATION, Device.getUiDevice()); this.ByMatcherClass = ReflectionUtils.getClass("androidx.test.uiautomator.ByMatcher"); - this.METHOD_FIND_MATCH = getMethod(ByMatcherClass, "findMatch", UiDevice.class, BySelector.class, AccessibilityNodeInfo[].class); - this.METHOD_FIND_MATCHES = getMethod(ByMatcherClass, "findMatches", UiDevice.class, BySelector.class, AccessibilityNodeInfo[].class); - this.uiObject2Constructor = getConstructor(UiObject2.class, UiDevice.class, BySelector.class, AccessibilityNodeInfo.class); + this.METHOD_FIND_MATCH = getMethod( + ByMatcherClass, "findMatch", + UiDevice.class, BySelector.class, AccessibilityNodeInfo[].class + ); + this.METHOD_FIND_MATCHES = getMethod( + ByMatcherClass, "findMatches", + UiDevice.class, BySelector.class, AccessibilityNodeInfo[].class + ); + this.uiObject2Constructor = getConstructor( + UiObject2.class, + UiDevice.class, BySelector.class, AccessibilityNodeInfo.class + ); + this.nativeGestureController = getNativeGestureControllerInstance(); } public static synchronized CustomUiDevice getInstance() { @@ -96,17 +110,6 @@ public UiAutomation getUiAutomation() { } private UiObject2 toUiObject2(@NonNull BySelector selector, @Nullable AccessibilityNodeInfo node) { - // TODO: remove this comment after upgrading to androidx.test.uiautomator:uiautomator:2.3.0 - // UiObject2 with androidx.test.uiautomator:uiautomator:2.3.0 has below code to crate the instance, - // thus if the node was None, it should create an empty element for the AccessibilityNodeInfo. - //
-        //    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-        //        AccessibilityWindowInfo window = UiObject2.Api21Impl.getWindow(cachedNode);
-        //        mDisplayId = window == null ? Display.DEFAULT_DISPLAY : UiObject2.Api30Impl.getDisplayId(window);
-        //    } else {
-        //        mDisplayId = Display.DEFAULT_DISPLAY;
-        //    }
-        // 
AccessibilityNodeInfo accessibilityNodeInfo = (node == null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) ? new AccessibilityNodeInfo() @@ -151,36 +154,26 @@ public AccessibleUiObject findObject(Object selector) throws UiAutomator2Excepti return node == null ? null : new AccessibleUiObject(toUiObject2(realSelector, node), node); } - public synchronized GestureController getGestureController() { - if (gestureController == null) { - Class gesturesClass = ReflectionUtils.getClass("androidx.test.uiautomator.Gestures"); - // TODO: UIAutomator lib has changed this class significantly in v2.3.0, - // TODO: so this approach won't work anymore - Method gesturesFactory = ReflectionUtils.getMethod( - gesturesClass, "getInstance", UiDevice.class - ); - Gestures gestures; - try { - gestures = new Gestures(gesturesFactory.invoke(gesturesClass, getUiDevice())); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new UiAutomator2Exception("Cannot get an instance of the Gestures class", e); - } - Class gestureControllerClass = ReflectionUtils.getClass( - "androidx.test.uiautomator.GestureController" - ); - Method gestureControllerFactory = ReflectionUtils.getMethod( - gestureControllerClass, "getInstance", UiDevice.class - ); - try { - gestureController = new GestureController( - gestureControllerFactory.invoke(gestureControllerClass, getUiDevice()), - gestures - ); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new UiAutomator2Exception("Cannot get an instance of the GestureController class", e); - } + private Object getNativeGestureControllerInstance() { + Class gestureControllerClass = ReflectionUtils.getClass("androidx.test.uiautomator.GestureController"); + Method gestureControllerFactory = ReflectionUtils.getMethod(gestureControllerClass, "getInstance", UiDevice.class); + try { + return gestureControllerFactory.invoke(gestureControllerClass, getUiDevice()); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new UiAutomator2Exception("Cannot get an instance of the GestureController class", e); } - return gestureController; + } + + public GestureController getGestureController(int displayId) { + return new GestureController(nativeGestureController, displayId); + } + + public GestureController getGestureController() { + return new GestureController(nativeGestureController); + } + + public GestureController getGestureController(AndroidElement element) { + return getGestureController(element.getDisplayId()); } /** @@ -226,4 +219,40 @@ public ScreenRotation setRotationSync(ScreenRotation desired) { throw new InvalidElementStateException(String.format("Screen rotation cannot be changed to %s after %sms. " + "Is it locked programmatically?", desired, CHANGE_ROTATION_TIMEOUT_MS)); } + + @Nullable + public Display getDisplayById(int displayId) { + Method getDisplayByIdMethod = ReflectionUtils.getMethod( + UiDevice.class, "getDisplayById", int.class + ); + try { + return (Display) getDisplayByIdMethod.invoke(getUiDevice(), displayId); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new UiAutomator2Exception(e); + } + } + + public Point getDisplaySize(int displayId) { + Method getDisplaySizeMethod = ReflectionUtils.getMethod( + UiDevice.class, "getDisplaySize", int.class + ); + try { + return (Point) getDisplaySizeMethod.invoke(getUiDevice(), displayId); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new UiAutomator2Exception(e); + } + } + + public int getTopmostWindowDisplayId() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + SparseArray> windowsMap = getUiAutomation().getWindowsOnAllDisplays(); + for (int i = 0; i < windowsMap.size(); i++) { + int displayId = windowsMap.keyAt(i); + if (displayId >= 0) { + return displayId; + } + } + } + return Display.DEFAULT_DISPLAY; + } } diff --git a/app/src/main/java/io/appium/uiautomator2/model/internal/GestureController.java b/app/src/main/java/io/appium/uiautomator2/model/internal/GestureController.java index 1d00b9d52..b824df84f 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/internal/GestureController.java +++ b/app/src/main/java/io/appium/uiautomator2/model/internal/GestureController.java @@ -31,19 +31,23 @@ import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.util.Arrays; import static io.appium.uiautomator2.utils.ReflectionUtils.invoke; public class GestureController { - private final Object wrappedInstance; private final Method performGestureMethod; private final Gestures gestures; - GestureController(Object wrappedInstance, Gestures gestures) { + GestureController(Object wrappedInstance, int displayId) { this.wrappedInstance = wrappedInstance; this.performGestureMethod = extractPerformGestureMethod(wrappedInstance); - this.gestures = gestures; + this.gestures = new Gestures(displayId); + } + + GestureController(Object wrappedInstance) { + this(wrappedInstance, CustomUiDevice.getInstance().getTopmostWindowDisplayId()); } private static Method extractPerformGestureMethod(Object wrappedInstance) { @@ -70,7 +74,7 @@ private static UiDevice getDevice() { } private class GestureRunnable implements Runnable { - private PointerGesture[] mGestures; + private final PointerGesture[] mGestures; public GestureRunnable(PointerGesture[] gestures) { mGestures = gestures; @@ -80,6 +84,11 @@ public GestureRunnable(PointerGesture[] gestures) { public void run() { performGesture(mGestures); } + + @Override + public String toString() { + return Arrays.toString(mGestures); + } } private R performGestureAndWait(EventCondition condition, long timeout, PointerGesture... gestures) { @@ -87,13 +96,13 @@ private R performGestureAndWait(EventCondition condition, long timeout, P } public void click(Point point) { - performGesture(new PointerGesture(point).pause(0L)); + performGesture(new PointerGesture(point, gestures.getDisplayId()).pause(0L)); } public void doubleClick(Point point) { - performGesture(new PointerGesture(point).pause(0L)); + performGesture(new PointerGesture(point, gestures.getDisplayId()).pause(0L)); SystemClock.sleep(ViewConfiguration.getDoubleTapTimeout() / 2); - performGesture(new PointerGesture(point).pause(0L)); + performGesture(new PointerGesture(point, gestures.getDisplayId()).pause(0L)); } public void longClick(Point point, @Nullable Long durationMs) { @@ -101,7 +110,7 @@ public void longClick(Point point, @Nullable Long durationMs) { if (duration < 0) { throw new IllegalArgumentException("Long click duration cannot be negative"); } - performGesture(new PointerGesture(point).pause(duration)); + performGesture(new PointerGesture(point, gestures.getDisplayId()).pause(duration)); } private static int checkSpeed(int speed) { @@ -145,17 +154,17 @@ public boolean scroll(Rect area, Direction direction, float percent, @Nullable I Direction swipeDirection = Direction.reverse(direction); int scrollSpeed = speed == null ? Gestures.getDefaultScrollSpeed() : checkSpeed(speed); - float swipePercent = percent; - while (swipePercent > 0.0f) { + for (float swipePercent = percent; swipePercent > 0.0f; swipePercent -= 1.0f) { float segment = Math.min(swipePercent, 1.0f); PointerGesture swipe = gestures.swipe(area, swipeDirection, segment, scrollSpeed).pause(250); // Perform the gesture and return early if we reached the end - if (performGestureAndWait(Until.scrollFinished(direction), Gestures.getScrollTimeout(), swipe)) { + Boolean scrollFinishedResult = performGestureAndWait( + Until.scrollFinished(direction), Gestures.getScrollTimeout(), swipe + ); + if (!Boolean.FALSE.equals(scrollFinishedResult)) { return false; } - - swipePercent -= 1.0f; } // We never reached the end return true; @@ -167,7 +176,8 @@ public boolean fling(Rect area, Direction direction, @Nullable Integer speed) { int flingSpeed = speed == null ? Gestures.getDefaultFlingSpeed() : speed; if (flingSpeed < minVelocity) { throw new IllegalArgumentException(String.format( - "Speed %s is less than the minimum fling velocity %s", speed, minVelocity)); + "Speed %s is less than the minimum fling velocity %s", speed, minVelocity) + ); } // To fling, we swipe in the opposite direction @@ -175,6 +185,11 @@ public boolean fling(Rect area, Direction direction, @Nullable Integer speed) { PointerGesture swipe = gestures.swipe(area, swipeDirection, 1.0f, flingSpeed); // Perform the gesture and return true if we did not reach the end - return !performGestureAndWait(Until.scrollFinished(direction), Gestures.getFlingTimeout(), swipe); + Boolean scrollFinishedResult = performGestureAndWait( + Until.scrollFinished(direction), + Gestures.getFlingTimeout(), + swipe + ); + return Boolean.FALSE.equals(scrollFinishedResult); } } diff --git a/app/src/main/java/io/appium/uiautomator2/model/internal/Gestures.java b/app/src/main/java/io/appium/uiautomator2/model/internal/Gestures.java index 4fd42d0a8..e64c89366 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/internal/Gestures.java +++ b/app/src/main/java/io/appium/uiautomator2/model/internal/Gestures.java @@ -20,7 +20,6 @@ import android.graphics.Point; import android.graphics.Rect; - import androidx.test.uiautomator.Direction; import androidx.test.uiautomator.UiObject2; @@ -33,43 +32,70 @@ import static io.appium.uiautomator2.utils.ReflectionUtils.getMethod; import static io.appium.uiautomator2.utils.ReflectionUtils.invoke; +import io.appium.uiautomator2.utils.ReflectionUtils; + public class Gestures { - private final Object wrappedInstance; + private final Class wrappedClass; + private final int displayId; - Gestures(Object wrappedInstance) { - this.wrappedInstance = wrappedInstance; + Gestures(int displayId) { + this.displayId = displayId; + // https://androidx.tech/artifacts/test.uiautomator/uiautomator/2.3.0-beta01-source/androidx/test/uiautomator/Gestures.java.html + this.wrappedClass = ReflectionUtils.getClass("androidx.test.uiautomator.Gestures"); } - public PointerGesture drag(Point start, Point end, int speed) { - Method dragMethod = getMethod(wrappedInstance.getClass(), "drag", - Point.class, Point.class, int.class); - return new PointerGesture(invoke(dragMethod, wrappedInstance, start, end, speed)); + public int getDisplayId() { + return displayId; } - private static PointerGesture[] toGesturesArray(Object result) { - List list = new ArrayList<>(); - for (int i = 0; i < Array.getLength(result); ++i) { - list.add(new PointerGesture(Array.get(result, i))); - } - return list.toArray(new PointerGesture[0]); + public PointerGesture drag(Point start, Point end, int speed) { + Method dragMethod = getMethod( + wrappedClass, "drag", + Point.class, Point.class, int.class, int.class + ); + return new PointerGesture( + invoke(dragMethod, wrappedClass, start, end, speed, displayId), + displayId + ); } public PointerGesture[] pinchClose(Rect area, float percent, int speed) { - Method pinchCloseMethod = getMethod(wrappedInstance.getClass(), "pinchClose", - Rect.class, float.class, int.class); - return toGesturesArray(invoke(pinchCloseMethod, wrappedInstance, area, percent, speed)); + Method pinchCloseMethod = getMethod( + wrappedClass, "pinchClose", + Rect.class, float.class, int.class, int.class + ); + return toGesturesArray( + invoke(pinchCloseMethod, wrappedClass, area, percent, speed, displayId) + ); } public PointerGesture[] pinchOpen(Rect area, float percent, int speed) { - Method pinchOpenMethod = getMethod(wrappedInstance.getClass(), "pinchOpen", - Rect.class, float.class, int.class); - return toGesturesArray(invoke(pinchOpenMethod, wrappedInstance, area, percent, speed)); + Method pinchOpenMethod = getMethod( + wrappedClass, "pinchOpen", + Rect.class, float.class, int.class, int.class + ); + return toGesturesArray( + invoke(pinchOpenMethod, wrappedClass, area, percent, speed, displayId) + ); } public PointerGesture swipe(Rect area, Direction direction, float percent, int speed) { - Method swipeRectMethod = getMethod(wrappedInstance.getClass(), "swipeRect", - Rect.class, Direction.class, float.class, int.class); - return new PointerGesture(invoke(swipeRectMethod, wrappedInstance, area, direction, percent, speed)); + Method swipeRectMethod = getMethod( + wrappedClass, "swipeRect", + Rect.class, Direction.class, float.class, int.class, int.class + ); + return new PointerGesture( + invoke(swipeRectMethod, wrappedClass, area, direction, percent, speed, displayId), + displayId + ); + } + + private PointerGesture[] toGesturesArray(Object result) { + List list = new ArrayList<>(); + for (int i = 0; i < Array.getLength(result); ++i) { + list.add(new PointerGesture(Array.get(result, i), displayId)); + } + return list.toArray(new PointerGesture[0]); } public static float getDisplayDensity() { diff --git a/app/src/main/java/io/appium/uiautomator2/model/internal/PointerGesture.java b/app/src/main/java/io/appium/uiautomator2/model/internal/PointerGesture.java index d1b1cb729..070bbc197 100644 --- a/app/src/main/java/io/appium/uiautomator2/model/internal/PointerGesture.java +++ b/app/src/main/java/io/appium/uiautomator2/model/internal/PointerGesture.java @@ -43,15 +43,15 @@ public synchronized static Class getWrappedClass() { private static synchronized Constructor getWrappedConstructor() { if (pointerGestureConstructor == null) { - pointerGestureConstructor = getConstructor(getWrappedClass(), Point.class); + pointerGestureConstructor = getConstructor(getWrappedClass(), Point.class, int.class); } return pointerGestureConstructor; } - public PointerGesture(Object wrappedInstanceOrPoint) { + public PointerGesture(Object wrappedInstanceOrPoint, int displayId) { if (wrappedInstanceOrPoint instanceof Point) { try { - this.wrappedInstance = getWrappedConstructor().newInstance(wrappedInstanceOrPoint); + this.wrappedInstance = getWrappedConstructor().newInstance(wrappedInstanceOrPoint, displayId); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new IllegalStateException(String.format("Cannot perform gesture at %s", wrappedInstanceOrPoint), e); } @@ -69,4 +69,9 @@ public PointerGesture pause(long ms) { public Object getWrappedInstance() { return this.wrappedInstance; } + + @Override + public String toString() { + return wrappedInstance.toString(); + } } diff --git a/app/src/main/java/io/appium/uiautomator2/utils/gestures/Click.java b/app/src/main/java/io/appium/uiautomator2/utils/gestures/Click.java index 07950e563..70ad47c18 100644 --- a/app/src/main/java/io/appium/uiautomator2/utils/gestures/Click.java +++ b/app/src/main/java/io/appium/uiautomator2/utils/gestures/Click.java @@ -55,7 +55,7 @@ public static Object perform(ClickModel clickModel) { bounds.left + clickModel.offset.x.intValue(), bounds.top + clickModel.offset.y.intValue() ); - CustomUiDevice.getInstance().getGestureController().click(location); + CustomUiDevice.getInstance().getGestureController(element).click(location); } } return null; diff --git a/app/src/main/java/io/appium/uiautomator2/utils/gestures/DoubleClick.java b/app/src/main/java/io/appium/uiautomator2/utils/gestures/DoubleClick.java index 840e01cc3..3c4defaf7 100644 --- a/app/src/main/java/io/appium/uiautomator2/utils/gestures/DoubleClick.java +++ b/app/src/main/java/io/appium/uiautomator2/utils/gestures/DoubleClick.java @@ -55,7 +55,7 @@ public static Object perform(DoubleClickModel doubleClickModel) { Rect bounds = element.getBounds(); Point location = new Point(bounds.left + doubleClickModel.offset.x.intValue(), bounds.top + doubleClickModel.offset.y.intValue()); - CustomUiDevice.getInstance().getGestureController().doubleClick(location); + CustomUiDevice.getInstance().getGestureController(element).doubleClick(location); } } return null; diff --git a/app/src/main/java/io/appium/uiautomator2/utils/gestures/LongClick.java b/app/src/main/java/io/appium/uiautomator2/utils/gestures/LongClick.java index 47990f4fd..314a5116e 100644 --- a/app/src/main/java/io/appium/uiautomator2/utils/gestures/LongClick.java +++ b/app/src/main/java/io/appium/uiautomator2/utils/gestures/LongClick.java @@ -60,7 +60,7 @@ public static Object perform(LongClickModel longClickModel) { Rect bounds = element.getBounds(); Point location = new Point(bounds.left + longClickModel.offset.x.intValue(), bounds.top + longClickModel.offset.y.intValue()); - CustomUiDevice.getInstance().getGestureController().longClick(location, + CustomUiDevice.getInstance().getGestureController(element).longClick(location, longClickModel.duration == null ? null : longClickModel.duration.longValue() ); }