/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.appium.android.bootstrap.handler; import com.android.uiautomator.core.UiObjectNotFoundException; import com.android.uiautomator.core.UiSelector; import android.os.Bundle; import static io.appium.android.bootstrap.utils.API.API_18; 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.Logger; import io.appium.android.bootstrap.WDStatus; import io.appium.android.bootstrap.exceptions.ElementNotFoundException; import io.appium.android.bootstrap.exceptions.InvalidSelectorException; import io.appium.android.bootstrap.exceptions.InvalidStrategyException; import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException; import io.appium.android.bootstrap.selector.Strategy; import io.appium.android.bootstrap.utils.ClassInstancePair; import io.appium.android.bootstrap.utils.ElementHelpers; import io.appium.android.bootstrap.utils.ReflectionUtils; import io.appium.android.bootstrap.utils.UiAutomatorParser; import io.appium.android.bootstrap.utils.XMLHierarchy; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * This handler is used to find elements in the Android UI. * <p/> * 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(); static JSONObject apkStrings = null; public static Bundle params = null; UiAutomatorParser uiAutomatorParser = new UiAutomatorParser(); /** * java_package : type / name * * com.example.Test:id/enter * * ^[a-zA-Z_] - Java package must start with letter or underscore * [a-zA-Z0-9\._]* - Java package may contain letters, numbers, periods and * underscores : - : ends the package and starts the type [^\/]+ - type is * made up of at least one non-/ characters \\/ - / ends the type and starts * the name [\S]+$ - the name contains at least one non-space character and * then the line is ended */ static final Pattern resourceIdRegex = Pattern .compile("^[a-zA-Z_][a-zA-Z0-9\\._]*:[^\\/]+\\/[\\S]+$"); /** * Get a JSONArray to represent a collection of AndroidElements * * @param els * collection of AndroidElement objects * @return elements in the format which appium server returns * @throws JSONException */ private JSONArray elementsToJSONArray(final List<AndroidElement> els) throws JSONException { final JSONArray resArray = new JSONArray(); for (final AndroidElement el : els) { resArray.put(ElementHelpers.toJSON(el)); } return resArray; } /* * @param command The {@link AndroidCommand} used for this handler. * * @return {@link AndroidCommandResult} * * @throws JSONException * * @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android. * bootstrap.AndroidCommand) */ @Override public AndroidCommandResult execute(final AndroidCommand command) throws JSONException { return execute(command, false); } /** * execute implementation. * * @see io.appium.android.bootstrap.handler.Find#execute(io.appium.android. * bootstrap.AndroidCommand) * * @param command * The {@link AndroidCommand} used for this handler. * * @param isRetry * Is this invocation a second attempt? * * @return {@link AndroidCommandResult} * @throws JSONException */ private AndroidCommandResult execute(final AndroidCommand command, final boolean isRetry) throws JSONException { final Hashtable<String, Object> params = command.params(); // only makes sense on a device final Strategy strategy; try { strategy = Strategy.fromString((String) params.get("strategy")); } catch (final InvalidStrategyException e) { return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage()); } final String contextId = (String) params.get("context"); final String text = (String) params.get("selector"); final boolean multiple = (Boolean) params.get("multiple"); Logger.debug("Finding '" + text + "' using '" + strategy.toString() + "' with the contextId: '" + contextId + "' multiple: " + multiple); boolean found = false; try { Object result = null; final List<UiSelector> selectors = getSelectors(strategy, text, multiple, contextId); if (!multiple) { for (int i = 0; i < selectors.size() && !found; i++) { try { Logger.debug("Using: " + selectors.get(i).toString()); result = fetchElement(selectors.get(i), contextId); found = result != null; } catch (final ElementNotFoundException ignored) { } } } else { List<AndroidElement> foundElements = new ArrayList<AndroidElement>(); for (final UiSelector sel : selectors) { // With multiple selectors, we expect that some elements may not // exist. try { Logger.debug("Using: " + sel.toString()); final List<AndroidElement> elementsFromSelector = fetchElements( sel, contextId); foundElements.addAll(elementsFromSelector); } catch (final UiObjectNotFoundException ignored) { } } if (strategy == Strategy.ANDROID_UIAUTOMATOR) { foundElements = ElementHelpers.dedupe(foundElements); } found = foundElements.size() > 0; result = elementsToJSONArray(foundElements); } if (!found) { if (!isRetry) { Logger .debug("Failed to locate element. Clearing Accessibility cache and retrying."); // some control updates fail to trigger AccessibilityEvents, resulting // in stale AccessibilityNodeInfo instances. In these cases, UIAutomator // will fail to locate visible elements. As a work-around, force clear // the AccessibilityInteractionClient's cache and search again. This // technique also appears to make Appium's searches conclude more quickly. // See Appium issue #4200 https://github.com/appium/appium/issues/4200 if (ReflectionUtils.clearAccessibilityCache()) { return execute(command, true); } } // JSONWP spec does not return NoSuchElement if (!multiple) { // If there are no results and we've already retried, return an error. return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, "No element found"); } } return getSuccessResult(result); } catch (final InvalidStrategyException e) { return getErrorResult(e.getMessage()); } catch (final UiSelectorSyntaxException e) { return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage()); } catch (final ElementNotFoundException e) { return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage()); } catch (final ParserConfigurationException e) { return getErrorResult("Error parsing xml hierarchy dump: " + e.getMessage()); } catch (final InvalidSelectorException e) { return new AndroidCommandResult(WDStatus.INVALID_SELECTOR, e.getMessage()); } } /** * Get the element from the {@link AndroidElementsHash} and return the element * id using JSON. * * @param sel * A UiSelector that targets the element to fetch. * @param contextId * The Id of the element used for the context. * @return JSONObject * @throws JSONException * @throws ElementNotFoundException */ private JSONObject fetchElement(final UiSelector sel, final String contextId) throws JSONException, ElementNotFoundException { final JSONObject res = new JSONObject(); final AndroidElement el = elements.getElement(sel, contextId); return res.put("ELEMENT", el.getId()); } /** * Get an array of AndroidElement objects from the {@link AndroidElementsHash} * * @param sel * A UiSelector that targets the element to fetch. * @param contextId * The Id of the element used for the context. * @return ArrayList<AndroidElement> * @throws UiObjectNotFoundException */ private ArrayList<AndroidElement> fetchElements(final UiSelector sel, final String contextId) throws UiObjectNotFoundException { return elements.getElements(sel, contextId); } /** * 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. * @param text * Any text used in the search (i.e. match, regex, etc.) * @param many * Boolean that is either only one element (false), or many (true) * @return UiSelector * @throws InvalidStrategyException * @throws ElementNotFoundException */ private List<UiSelector> getSelectors(final Strategy strategy, final String text, final boolean many, final String contextId) throws InvalidStrategyException, ElementNotFoundException, UiSelectorSyntaxException, ParserConfigurationException, InvalidSelectorException { final List<UiSelector> selectors = new ArrayList<UiSelector>(); UiSelector sel = new UiSelector(); switch (strategy) { case XPATH: for (final UiSelector selector : getXPathSelectors(text, many, contextId)) { selectors.add(selector); } break; case CLASS_NAME: sel = sel.className(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); break; case ID: // There are three types of ids on Android. // 1. resourceId (API >= 18) // 2. accessibility id (content description) // 3. strings.xml id // // If text is a resource id then only use the resource id selector. if (API_18) { if (resourceIdRegex.matcher(text).matches()) { sel = sel.resourceId(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); break; } else { // not a fully qualified resource id // transform "textToBeChanged" into: // com.example.android.testing.espresso.BasicSample:id/textToBeChanged // android:id/textToBeChanged // either it's prefixed with the app package or the android system page. String pkg = (String) params.get("pkg"); if (pkg != null) { sel = sel.resourceId(pkg + ":id/" + text); if (!many) { sel = sel.instance(0); } selectors.add(sel); } sel = sel.resourceId("android:id/" + text); if (!many) { sel = sel.instance(0); } selectors.add(sel); // webview element ids do not have a package prefix sel = sel.resourceId(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); } } // must create a new selector or the selector from // the resourceId search will cause problems sel = new UiSelector().description(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); // resource id and content description failed to match // so the strings.xml selector is used final UiSelector stringsXmlSelector = stringsXmlId(many, text); if (stringsXmlSelector != null) { selectors.add(stringsXmlSelector); } break; case ACCESSIBILITY_ID: sel = sel.description(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); break; case NAME: sel = new UiSelector().description(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); sel = new UiSelector().text(text); if (!many) { sel = sel.instance(0); } selectors.add(sel); break; case ANDROID_UIAUTOMATOR: List<UiSelector> parsedSelectors; try { parsedSelectors = uiAutomatorParser.parse(text); } catch (final UiSelectorSyntaxException e) { throw new UiSelectorSyntaxException( "Could not parse UiSelector argument: " + e.getMessage()); } for (final UiSelector selector : parsedSelectors) { selectors.add(selector); } break; case LINK_TEXT: case PARTIAL_LINK_TEXT: case CSS_SELECTOR: default: throw new InvalidStrategyException("Sorry, we don't support the '" + strategy.getStrategyName() + "' locator strategy yet"); } return selectors; } /** returns List of UiSelectors for an xpath expression **/ private List<UiSelector> getXPathSelectors(final String expression, final boolean multiple, String contextId) throws ElementNotFoundException, ParserConfigurationException, InvalidSelectorException { final List<UiSelector> selectors = new ArrayList<UiSelector>(); final ArrayList<ClassInstancePair> pairs = contextId.equals("") ? XMLHierarchy.getClassInstancePairs(expression) : XMLHierarchy.getClassInstancePairs(expression, contextId); if (!multiple) { if (pairs.size() == 0) { throw new ElementNotFoundException(); } selectors.add(pairs.get(0).getSelector()); } else { for (final ClassInstancePair pair : pairs) { selectors.add(pair.getSelector()); } } return selectors; } /** Returns null on failure to match **/ private UiSelector stringsXmlId(final boolean many, final String text) { UiSelector sel = null; try { final String xmlValue = apkStrings.getString(text); if (xmlValue == null || xmlValue.isEmpty()) { return null; } sel = new UiSelector().text(xmlValue); if (!many) { sel = sel.instance(0); } } catch (final JSONException e) { } finally { return sel; } } }