From 8e9f002e9d7f74dfe1c57e23aa489735f1663501 Mon Sep 17 00:00:00 2001 From: bootstraponline Date: Fri, 30 May 2014 10:17:32 -0400 Subject: [PATCH] Add dedupe for complex_find --- .../uiautomator/common/ReflectionUtils.java | 18 +-- .../io/appium/android/bootstrap/Dynamic.java | 5 +- .../android/bootstrap/handler/Find.java | 105 ++++++++---------- .../bootstrap/handler/LongPressKeyCode.java | 2 +- .../handler/MultiPointerGesture.java | 2 +- .../android/bootstrap/handler/TouchDown.java | 2 +- .../bootstrap/handler/TouchLongClick.java | 4 +- .../android/bootstrap/handler/TouchMove.java | 2 +- .../android/bootstrap/handler/TouchUp.java | 2 +- .../bootstrap/utils/ElementHelpers.java | 54 +++++++++ 10 files changed, 120 insertions(+), 76 deletions(-) create mode 100644 lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/ElementHelpers.java diff --git a/lib/devices/android/bootstrap/src/com/android/uiautomator/common/ReflectionUtils.java b/lib/devices/android/bootstrap/src/com/android/uiautomator/common/ReflectionUtils.java index 35ab0bc8d..8bf16b1ae 100644 --- a/lib/devices/android/bootstrap/src/com/android/uiautomator/common/ReflectionUtils.java +++ b/lib/devices/android/bootstrap/src/com/android/uiautomator/common/ReflectionUtils.java @@ -1,14 +1,12 @@ package com.android.uiautomator.common; +import android.os.Build; +import com.android.uiautomator.core.UiDevice; import io.appium.android.bootstrap.Logger; import java.lang.reflect.Field; import java.lang.reflect.Method; -import android.os.Build; - -import com.android.uiautomator.core.UiDevice; - public class ReflectionUtils { private static Field enableField(final Class clazz, final String field) throws SecurityException, NoSuchFieldException { @@ -44,13 +42,15 @@ public class ReflectionUtils { return controller; } - public Method getMethod(final String name, final Class... parameterTypes) + public Method getControllerMethod(final String name, final Class... parameterTypes) throws NoSuchMethodException, SecurityException { - final Class controllerClass = controller.getClass(); + return getMethod(controller.getClass(), name, parameterTypes); + } - Logger.debug("Finding methods on class: " + controllerClass); - final Method method; - method = controllerClass.getDeclaredMethod(name, parameterTypes); + public Method getMethod(final Class clazz, String name, final Class... parameterTypes) + throws NoSuchMethodException, SecurityException { + Logger.debug("Finding methods on class: " + clazz); + final Method method = clazz.getDeclaredMethod(name, parameterTypes); method.setAccessible(true); return method; diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/Dynamic.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/Dynamic.java index 5615bde15..45757f1e9 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/Dynamic.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/Dynamic.java @@ -1,6 +1,7 @@ package io.appium.android.bootstrap; import java.util.ArrayList; +import java.util.List; import org.json.JSONArray; import org.json.JSONException; @@ -90,8 +91,8 @@ public class Dynamic { return value; } - public static ArrayList finalize( - final ArrayList elements, final int finalizer) + public static List finalize( + final List elements, final int finalizer) throws Exception { final ArrayList results = new ArrayList(); for (final AndroidElement e : elements) { diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java index 42844353c..5042c2b2f 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java @@ -1,52 +1,37 @@ package io.appium.android.bootstrap.handler; -import io.appium.android.bootstrap.AndroidCommand; -import io.appium.android.bootstrap.AndroidCommandResult; -import io.appium.android.bootstrap.AndroidElement; -import io.appium.android.bootstrap.AndroidElementsHash; -import io.appium.android.bootstrap.CommandHandler; -import io.appium.android.bootstrap.Dynamic; -import io.appium.android.bootstrap.Logger; -import io.appium.android.bootstrap.WDStatus; -import io.appium.android.bootstrap.exceptions.AndroidCommandException; -import io.appium.android.bootstrap.exceptions.ElementNotFoundException; -import io.appium.android.bootstrap.exceptions.ElementNotInHashException; -import io.appium.android.bootstrap.exceptions.InvalidStrategyException; -import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException; -import io.appium.android.bootstrap.exceptions.UnallowedTagNameException; +import android.os.Build; +import com.android.uiautomator.core.UiObject; +import com.android.uiautomator.core.UiObjectNotFoundException; +import com.android.uiautomator.core.UiScrollable; +import com.android.uiautomator.core.UiSelector; +import io.appium.android.bootstrap.*; +import io.appium.android.bootstrap.exceptions.*; import io.appium.android.bootstrap.selector.Strategy; +import io.appium.android.bootstrap.utils.ElementHelpers; import io.appium.android.bootstrap.utils.NotImportantViews; import io.appium.android.bootstrap.utils.UiSelectorParser; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import java.util.List; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.os.Build; - -import com.android.uiautomator.core.UiObject; -import com.android.uiautomator.core.UiObjectNotFoundException; -import com.android.uiautomator.core.UiScrollable; -import com.android.uiautomator.core.UiSelector; - /** * This handler is used to find elements in the Android UI. - * + *

* Based on which {@link Strategy}, {@link UiSelector}, and optionally the * contextId, the element Id or Ids are returned to the user. - * */ public class Find extends CommandHandler { // These variables are expected to persist across executions. - AndroidElementsHash elements = AndroidElementsHash.getInstance(); - Dynamic dynamic = new Dynamic(); - public static JSONObject apkStrings = null; - UiSelectorParser uiSelectorParser = new UiSelectorParser(); + AndroidElementsHash elements = AndroidElementsHash.getInstance(); + Dynamic dynamic = new Dynamic(); + public static JSONObject apkStrings = null; + UiSelectorParser uiSelectorParser = new UiSelectorParser(); /* * @param command The {@link AndroidCommand} used for this handler. @@ -104,7 +89,8 @@ public class Find extends CommandHandler { try { int finalizer = 0; JSONArray pair = null; - final JSONArray results = new JSONArray(); + List elementResults = new ArrayList(); + final JSONArray jsonResults = new JSONArray(); // Start at 1 to skip over all. for (int selIndex = all || scroll ? 1 : 0; selIndex < selectors .length(); selIndex++) { @@ -128,11 +114,11 @@ public class Find extends CommandHandler { if (finalizer != 0) { if (all) { Logger.debug("Finding all with finalizer"); - final ArrayList eles = elements.getElements( + List eles = elements.getElements( sel, contextId); Logger.debug("Elements found: " + eles); for (final String found : Dynamic.finalize(eles, finalizer)) { - results.put(found); + jsonResults.put(found); } continue; } else { @@ -143,10 +129,8 @@ public class Find extends CommandHandler { } if (all) { - final ArrayList els = elements.getElements(sel, - contextId); - for (final AndroidElement el : els) { - results.put(new JSONObject().put("ELEMENT", el.getId())); + for (AndroidElement e : elements.getElements(sel, contextId)) { + elementResults.add(e); } continue; } else if (scroll && canScroll) { @@ -163,9 +147,16 @@ public class Find extends CommandHandler { } catch (final ElementNotFoundException enf) { Logger.debug("Not found."); } - } - if (all && results.length() > 0) { - return getSuccessResult(results); + } // end for loop + if (all) { + // matching on multiple selectors may return duplicate elements + elementResults = ElementHelpers.dedupe(elementResults); + + for (final AndroidElement el : elementResults) { + jsonResults.put(new JSONObject().put("ELEMENT", el.getId())); + } + + return getSuccessResult(jsonResults); } return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, "No such element."); @@ -173,7 +164,7 @@ public class Find extends CommandHandler { final String errorMessage = e.getMessage(); if (errorMessage != null && errorMessage - .contains("UiAutomationService not connected. Did you call #register()?")) { + .contains("UiAutomationService not connected. Did you call #register()?")) { // Crash on not connected so Appium restarts the bootstrap jar. throw new RuntimeException(e); } @@ -247,12 +238,11 @@ public class Find extends CommandHandler { /** * Get the element from the {@link AndroidElementsHash} and return the element * id using JSON. - * + * * @param sel - * A UiSelector that targets the element to fetch. + * A UiSelector that targets the element to fetch. * @param contextId - * The Id of the element used for the context. - * + * The Id of the element used for the context. * @return JSONObject * @throws JSONException * @throws ElementNotFoundException @@ -268,7 +258,7 @@ public class Find extends CommandHandler { /** * Get a single element by its index and its parent indexes. Used to resolve * an xpath query - * + * * @param indexPath * @return * @throws ElementNotInHashException @@ -295,12 +285,11 @@ public class Find extends CommandHandler { /** * Get an array of elements from the {@link AndroidElementsHash} and return * the element's ids using JSON. - * + * * @param sel - * A UiSelector that targets the element to fetch. + * A UiSelector that targets the element to fetch. * @param contextId - * The Id of the element used for the context. - * + * The Id of the element used for the context. * @return JSONObject * @throws JSONException * @throws UiObjectNotFoundException @@ -320,12 +309,12 @@ public class Find extends CommandHandler { /** * Get a find element result by looking through the paths of indexes used to * retrieve elements from an XPath search - * + * * @param selector * @return */ private AndroidCommandResult findElementsByIndexPaths(final String selector, - final Boolean multiple) { + final Boolean multiple) { final ArrayList indexPaths = new ArrayList( Arrays.asList(selector.split(","))); final JSONArray resArray = new JSONArray(); @@ -354,20 +343,20 @@ public class Find extends CommandHandler { /** * Create and return a UiSelector based on the strategy, text, and how many * you want returned. - * + * * @param strategy - * The {@link Strategy} used to search for the element. + * The {@link Strategy} used to search for the element. * @param text - * Any text used in the search (i.e. match, regex, etc.) + * Any text used in the search (i.e. match, regex, etc.) * @param many - * Boolean that is either only one element (false), or many (true) + * Boolean that is either only one element (false), or many (true) * @return UiSelector * @throws InvalidStrategyException * @throws AndroidCommandException * @throws ElementNotFoundException */ private List getSelector(final Strategy strategy, - final String text, final boolean many) throws InvalidStrategyException, + final String text, final boolean many) throws InvalidStrategyException, AndroidCommandException, UnallowedTagNameException, ElementNotFoundException, UiSelectorSyntaxException { final List selectors = new ArrayList(); diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/LongPressKeyCode.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/LongPressKeyCode.java index 0badc8513..b32488558 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/LongPressKeyCode.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/LongPressKeyCode.java @@ -42,7 +42,7 @@ public class LongPressKeyCode extends CommandHandler { throws JSONException { try { final ReflectionUtils utils = new ReflectionUtils(); - final Method injectEventSync = utils.getMethod("injectEventSync", + final Method injectEventSync = utils.getControllerMethod("injectEventSync", InputEvent.class); final Hashtable params = command.params(); keyCode = (Integer) params.get("keycode"); diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/MultiPointerGesture.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/MultiPointerGesture.java index 968d8c8a4..7ca883a41 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/MultiPointerGesture.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/MultiPointerGesture.java @@ -68,7 +68,7 @@ public class MultiPointerGesture extends CommandHandler { } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { final ReflectionUtils utils = new ReflectionUtils(); - final Method pmpg = utils.getMethod("performMultiPointerGesture", + final Method pmpg = utils.getControllerMethod("performMultiPointerGesture", PointerCoords[][].class); final Boolean rt = (Boolean) pmpg.invoke(utils.getController(), (Object) pcs); diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchDown.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchDown.java index 75b27bd86..173e8960e 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchDown.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchDown.java @@ -19,7 +19,7 @@ public class TouchDown extends TouchEvent { printEventDebugLine("TouchDown"); try { final ReflectionUtils utils = new ReflectionUtils(); - final Method touchDown = utils.getMethod("touchDown", int.class, + final Method touchDown = utils.getControllerMethod("touchDown", int.class, int.class); return (Boolean) touchDown.invoke(utils.getController(), clickX, clickY); } catch (final Exception e) { diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java index 298413ab0..7b5916386 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java @@ -26,9 +26,9 @@ public class TouchLongClick extends TouchEvent { */ final ReflectionUtils utils = new ReflectionUtils(); - final Method touchDown = utils.getMethod("touchDown", int.class, + final Method touchDown = utils.getControllerMethod("touchDown", int.class, int.class); - final Method touchUp = utils.getMethod("touchUp", int.class, int.class); + final Method touchUp = utils.getControllerMethod("touchUp", int.class, int.class); if ((Boolean) touchDown.invoke(utils.getController(), x, y)) { SystemClock.sleep(duration); diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchMove.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchMove.java index ebb6ec4e0..e85788597 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchMove.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchMove.java @@ -19,7 +19,7 @@ public class TouchMove extends TouchEvent { printEventDebugLine("TouchMove"); try { final ReflectionUtils utils = new ReflectionUtils(); - final Method touchMove = utils.getMethod("touchMove", int.class, + final Method touchMove = utils.getControllerMethod("touchMove", int.class, int.class); return (Boolean) touchMove.invoke(utils.getController(), clickX, clickY); } catch (final Exception e) { diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchUp.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchUp.java index 0cb240af9..a430db13e 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchUp.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchUp.java @@ -19,7 +19,7 @@ public class TouchUp extends TouchEvent { printEventDebugLine("TouchUp"); try { final ReflectionUtils utils = new ReflectionUtils(); - final Method touchUp = utils.getMethod("touchUp", int.class, int.class); + final Method touchUp = utils.getControllerMethod("touchUp", int.class, int.class); return (Boolean) touchUp.invoke(utils.getController(), clickX, clickY); } catch (final Exception e) { Logger.debug("Problem invoking touchUp: " + e); diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/ElementHelpers.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/ElementHelpers.java new file mode 100644 index 000000000..cc245f8d8 --- /dev/null +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/ElementHelpers.java @@ -0,0 +1,54 @@ +package io.appium.android.bootstrap.utils; + +import android.view.accessibility.AccessibilityNodeInfo; +import com.android.uiautomator.common.ReflectionUtils; +import com.android.uiautomator.core.UiObject; +import io.appium.android.bootstrap.AndroidElement; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +public abstract class ElementHelpers { + + private static Method findAccessibilityNodeInfo; + + private static AccessibilityNodeInfo elementToNode(AndroidElement element) { + AccessibilityNodeInfo result = null; + try { + result = (AccessibilityNodeInfo) findAccessibilityNodeInfo.invoke(element.getUiObject(), 5000L); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } + + /** + * Remove all duplicate elements from the provided list + * + * @param elements - elements to remove duplicates from + * @return a new list with duplicates removed + */ + public static List dedupe(List elements) { + try { + ReflectionUtils utils = new ReflectionUtils(); + findAccessibilityNodeInfo = utils.getMethod(UiObject.class, "findAccessibilityNodeInfo", long.class); + } catch (Exception e) { + e.printStackTrace(); + } + + List result = new ArrayList(); + List nodes = new ArrayList(); + + for (int index = 0; index < elements.size(); index++) { + AndroidElement element = elements.get(index); + AccessibilityNodeInfo node = elementToNode(element); + if (!nodes.contains(node)) { + nodes.add(node); + result.add(element); + } + } + + return result; + } +}