/* * 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.utils; 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.Logger; import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; /** * For parsing strings that create UiScrollable objects into UiScrollable objects */ public class UiScrollableParser { private String text; private UiScrollable scrollable; private UiObject uiObject; private boolean returnedUiObject; private final static Method[] methods = UiScrollable.class.getDeclaredMethods(); private static String[] prefixes = {"new UiScrollable", "UiScrollable"}; /* * Returns whether or not the input string is trying to instantiate a UiScrollable, and use its methods */ public static boolean isUiScrollable(String textToParse) { for (String prefix : prefixes) { if (textToParse.startsWith(prefix)) { return true; } } return false; } /* * Parse a string into a UiSelector, but use UiScrollable class and methods */ public UiSelector parse(String textToParse) throws UiSelectorSyntaxException { text = textToParse.trim(); returnedUiObject = false; consumePrefix(); consumeConstructor(); while (text.length() > 0) { consumePeriod(); consumeFunctionCall(); } if (!returnedUiObject) { throw new UiSelectorSyntaxException("Last method called on a UiScrollable object must return a UiObject object"); } return uiObject.getSelector(); } private void consumePeriod() throws UiSelectorSyntaxException { if (text.startsWith(".")) { text = text.substring(1); } else { throw new UiSelectorSyntaxException("Expected \".\" but saw \"" + text.charAt(0) + "\""); } } /* * You can start a UiScrollable like: "new UiScrollable(UiSelector).somemethod()" or "Uiscrollable(UiSelector).somemethod()" */ private void consumePrefix() throws UiSelectorSyntaxException { boolean removedPrefix = false; for (String prefix : prefixes) { if (text.startsWith(prefix)) { text = text.substring(prefix.length()); removedPrefix = true; break; } } if (!removedPrefix) { throw new UiSelectorSyntaxException("Was trying to parse as UiScrollable, but didn't start with an acceptable prefix. Acceptable prefixes are: 'new UiScrollable' or 'UiScrollable'. Saw: " + text); } } /* * consume UiScrollable constructor argument: parens surrounding a uiSelector. eg - "(new UiSelector().scrollable(true))" * initialize the UiScrollable object for this parser */ private void consumeConstructor() throws UiSelectorSyntaxException { if (text.charAt(0) != '(') { throw new UiSelectorSyntaxException("Was expecting \"" + ")" + "\" but instead saw \"" + text.charAt(0) + "\"" ); } StringBuilder argument = new StringBuilder(); int index = 1; int parenCount = 1; while (parenCount > 0) { try { switch (text.charAt(index)) { case ')': parenCount--; if (parenCount > 0) { argument.append(text.charAt(index)); } break; case '(': parenCount++; argument.append(text.charAt(index)); break; default: argument.append(text.charAt(index)); } } catch (StringIndexOutOfBoundsException e) { throw new UiSelectorSyntaxException("unclosed paren in expression"); } index++; } if (argument.length() < 1) { throw new UiSelectorSyntaxException("UiScrollable constructor expects an argument"); } UiSelector selector = new UiSelectorParser().parse(argument.toString()); scrollable = new UiScrollable(selector); // add two for parentheses surrounding arg text = text.substring(argument.length() + 2); } /* * consume [a-z]* then an open paren, this is our methodName * consume .* and count open/close parens until the original open paren is close, this is our argument * */ private void consumeFunctionCall() throws UiSelectorSyntaxException { String methodName; StringBuilder argument = new StringBuilder(); int parenIndex = text.indexOf('('); methodName = text.substring(0, parenIndex); int index = parenIndex+1; int parenCount = 1; while (parenCount > 0) { try { switch (text.charAt(index)) { case ')': parenCount--; if (parenCount > 0) { argument.append(text.charAt(index)); } break; case '(': parenCount++; argument.append(text.charAt(index)); break; default: argument.append(text.charAt(index)); } } catch (StringIndexOutOfBoundsException e) { throw new UiSelectorSyntaxException("unclosed paren in expression"); } index++; } ArrayList<String> args = splitArgs(argument.toString()); Method method = getUiScrollableMethod(methodName, args); applyArgsToMethod(method, args); // add two for parentheses surrounding arg text = text.substring(methodName.length() + argument.length() + 2); } private Method getUiScrollableMethod(String methodName, Collection<String> args) throws UiSelectorSyntaxException { for (Method method : methods) { if (method.getName().equals(methodName) && method.getGenericParameterTypes().length == args.size()) { return method; } } throw new UiSelectorSyntaxException("UiScrollable has no \"" + methodName + "\" method that takes " + args.size() + " arguments"); } private void applyArgsToMethod(Method method, ArrayList<String> arguments) throws UiSelectorSyntaxException { StringBuilder sb = new StringBuilder(); for (String arg : arguments) { sb.append(arg + ", "); } Logger.debug("UiScrollable invoking method: " + method + " args: " + sb.toString()); if (method.getGenericReturnType() == UiScrollable.class && returnedUiObject) { throw new UiSelectorSyntaxException("Cannot call UiScrollable method \"" + method.getName() + "\" on a UiObject instance"); } if (method.getGenericParameterTypes().length == 0) { try { scrollable = (UiScrollable)method.invoke(scrollable); } catch (IllegalAccessException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } catch (InvocationTargetException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } catch (ClassCastException e) { throw new UiSelectorSyntaxException("methods must return UiScrollable or UiObject instances"); } } else { ArrayList<Object> convertedArgs = new ArrayList<Object>(); Type[] parameterTypes = method.getGenericParameterTypes(); for (int i = 0; i < parameterTypes.length; i++) { convertedArgs.add(coerceArgToType(parameterTypes[i], arguments.get(i))); } String methodName = method.getName(); Logger.debug("Method name: " + methodName); boolean scrollIntoView = methodName.contentEquals("scrollIntoView"); if (method.getGenericReturnType() == UiScrollable.class || scrollIntoView) { if (convertedArgs.size() > 1) { throw new UiSelectorSyntaxException("No UiScrollable method that returns type UiScrollable takes more than 1 argument"); } try { if (scrollIntoView) { Logger.debug("Setting uiObject for scrollIntoView"); UiSelector arg = (UiSelector) convertedArgs.get(0); returnedUiObject = true; uiObject = new UiObject(arg); if (!scrollable.exists()) { Logger.debug("Not scrolling because scrollable does not exist: " + scrollable.getSelector()); return; } // scrollIntoView must return the object if it's already in view. // without the exists check, the parser will error because there's no scrollable. if (uiObject.exists()) { return; } Logger.debug("Invoking method: " + method + " with: " + uiObject); method.invoke(scrollable, uiObject); Logger.debug("Invoke complete."); } else { scrollable = (UiScrollable)method.invoke(scrollable, convertedArgs.get(0)); } } catch (IllegalAccessException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } catch (InvocationTargetException e) { // Ignoring UiObjectNotFoundException as this handled during actual find. if (e.getCause() instanceof UiObjectNotFoundException) { Logger.debug("Ignoring UiObjectNotFoundException when using reflection to invoke method."); return; } Logger.error(e.getCause().toString()); // we're only interested in the cause. InvocationTarget wraps the underlying problem. throw new UiSelectorSyntaxException("problem using reflection to call this method"); } } else if (method.getGenericReturnType() == UiObject.class) { returnedUiObject = true; if (convertedArgs.size() == 2) { try { uiObject = (UiObject)method.invoke(scrollable, convertedArgs.get(0), convertedArgs.get(1)); } catch (IllegalAccessException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } catch (InvocationTargetException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } } else if (convertedArgs.size() == 3) { try { uiObject = (UiObject)method.invoke(scrollable, convertedArgs.get(0), convertedArgs.get(1), convertedArgs.get(2)); } catch (IllegalAccessException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } catch (InvocationTargetException e) { e.printStackTrace(); throw new UiSelectorSyntaxException("problem using reflection to call this method"); } } else { throw new UiSelectorSyntaxException("UiScrollable methods which return a UiObject have 2-3 args"); } } else { throw new UiSelectorSyntaxException("Must only call the 'scrollIntoView' method OR methods on UiScrollable which return UiScrollable or UiObject objects"); } } } private Object coerceArgToType(Type type, String argument) throws UiSelectorSyntaxException { Logger.debug("UiScrollable coerce type: " + type + " arg: " + argument); if (type == boolean.class) { if (argument.equals("true")) { return true; } if (argument.equals("false")) { return false; } throw new UiSelectorSyntaxException(argument + " is not a boolean"); } if (type == String.class) { if (argument.charAt(0) != '"' || argument.charAt(argument.length()-1) != '"') { throw new UiSelectorSyntaxException(argument + " is not a string"); } return argument.substring(1, argument.length()-1); } if (type == int.class) { return Integer.parseInt(argument); } if (type == double.class) { return Double.parseDouble(argument); } if (type.toString().equals("java.lang.Class<T>")) { try { return Class.forName(argument); } catch (ClassNotFoundException e) { throw new UiSelectorSyntaxException(argument + " class could not be found"); } } if (type == UiSelector.class || type == UiObject.class) { UiSelectorParser parser = new UiSelectorParser(); return parser.parse(argument); } throw new UiSelectorSyntaxException("Could not coerce " + argument + " to any sort of Type"); } private ArrayList<String> splitArgs(String argumentString) throws UiSelectorSyntaxException { ArrayList<String> args = new ArrayList<String>(); if (argumentString.isEmpty()) { return args; } if (argumentString.charAt(0) == ',' || argumentString.charAt(argumentString.length()-1) == ',') { throw new UiSelectorSyntaxException("Missing argument. Trying to parse: " + argumentString); } int prevIndex = 0; int index = 1; boolean inQuotes = false; while (index < argumentString.length()) { switch (argumentString.charAt(index)) { case ',': if (!inQuotes) { if (prevIndex == index) { throw new UiSelectorSyntaxException("Missing argument. Trying to parse: " + argumentString); } args.add(argumentString.substring(prevIndex, index).trim()); prevIndex = index+1; } case '"': inQuotes = !inQuotes; break; } index++; } args.add(argumentString.substring(prevIndex, index).trim()); return args; } }