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..1dd54cbad 100644 --- a/app/src/main/java/io/appium/uiautomator2/core/AxNodeInfoHelper.java +++ b/app/src/main/java/io/appium/uiautomator2/core/AxNodeInfoHelper.java @@ -23,6 +23,7 @@ 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; @@ -36,6 +37,7 @@ 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; @@ -128,12 +130,12 @@ private static Rect getBoundsForGestures(AccessibilityNodeInfo node) { 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 +144,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 +153,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 +162,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 (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,7 +185,7 @@ 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) { @@ -178,7 +194,7 @@ public static void swipe(AccessibilityNodeInfo node, Direction direction, float 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) { @@ -187,7 +203,7 @@ public static boolean scroll(AccessibilityNodeInfo node, Direction direction, fl 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) { @@ -196,7 +212,7 @@ public static boolean fling(AccessibilityNodeInfo node, Direction direction) { 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); } /** 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..d0f719c38 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 @@ -40,6 +40,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 +66,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 +106,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 +150,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()); } /** 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..6b733f8d9 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 @@ -21,6 +21,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.SystemClock; +import android.view.Display; import android.view.ViewConfiguration; import androidx.annotation.Nullable; @@ -35,15 +36,28 @@ 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, getCurrentDisplayId()); + } + + private static int getCurrentDisplayId() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + Display display = getInstrumentation().getTargetContext().getDisplay(); + if (display != null) { + return display.getDisplayId(); + } + } + return Display.DEFAULT_DISPLAY; } private static Method extractPerformGestureMethod(Object wrappedInstance) { @@ -70,7 +84,7 @@ private static UiDevice getDevice() { } private class GestureRunnable implements Runnable { - private PointerGesture[] mGestures; + private final PointerGesture[] mGestures; public GestureRunnable(PointerGesture[] gestures) { mGestures = gestures; 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..5a6d6ec77 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,62 @@ 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)); - } - - 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]); + Method dragMethod = getMethod( + wrappedClass, "drag", + Point.class, Point.class, int.class, int.class + ); + return new PointerGesture(invoke(dragMethod, wrappedClass, start, end, speed, 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) + ); + } + + 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 static float getDisplayDensity() { 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() ); }