/* MonkeyTalk - a cross-platform functional testing tool
Copyright (C) 2012 Gorilla Logic, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package com.gorillalogic.fonemonkey.automators;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.app.Application;
import android.app.Dialog;
import android.content.ComponentName;
import android.os.Environment;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.webkit.WebView;
import com.gorillalogic.fonemonkey.ActivityManager;
import com.gorillalogic.fonemonkey.FunctionalityAdder;
import com.gorillalogic.fonemonkey.Log;
import com.gorillalogic.fonemonkey.PropertyUtil;
import com.gorillalogic.fonemonkey.Recorder;
import com.gorillalogic.fonemonkey.exceptions.FoneMonkeyScriptFailure;
import com.gorillalogic.fonemonkey.web.HtmlElement;
import com.gorillalogic.monkeytalk.BuildStamp;
import com.gorillalogic.monkeytalk.automators.AutomatorConstants;
public class AutomationManager {
static boolean isInitialized = false;
private static String ignoreNextAction = null;
private static View ignoreNextView = null;
@SuppressWarnings("unused")
private static Recorder recorder = new Recorder();
static void logVersion() {
Log.log(BuildStamp.STAMP);
}
/**
* This method will be called by internal FoneMonkey code unless the other init() method has
* been called already.
*/
public static void init() {
if (isInitialized)
return;
isInitialized = true;
logVersion();
try {
loadAutomators(null);
} catch (Exception e) {
Log.log(e);
}
}
/**
* Client code should call this method upon app launch if you wish to specify an automators
* control file that is packaged with the app.
*/
public static void init(InputStream customAutomatorsSource) throws IOException {
automators.clear();
automatorsByType.clear();
isInitialized = true;
logVersion();
try {
loadAutomators(customAutomatorsSource);
} catch (Exception e) {
Log.log(e);
}
}
static private void loadAutomators(InputStream customAutomatorsSource) throws Exception {
InputStream is = AutomationManager.class.getResourceAsStream("/DefaultAutomators.txt");
if (is == null) {
Log.log("ERROR: Your APK does not seem to be linked with the MonkeyTalk library");
return;
}
InputStream extensions = AutomationManager.class
.getResourceAsStream("/monkeytalk.automators");
if (extensions != null) {
is = new SequenceInputStream(is, extensions);
}
String line;
BufferedReader in = new BufferedReader(new InputStreamReader(is));
boolean noOverrides = false;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.startsWith("#") || line.length() == 0) {
continue;
}
Class<?> klass;
try {
klass = (Class<?>) Class.forName(line);
} catch (Exception e) {
Log.log("Unable to load automator " + line, e);
continue;
}
IAutomator ia = (IAutomator) klass.newInstance();
registerClass(ia.getComponentType(), ia.getComponentClass(), klass, ia.getAliases(),
ia.isHtmlAutomator());
}
in.close();
// We give preference to loading from a source provided by the app
if (customAutomatorsSource != null) {
in = new BufferedReader(new InputStreamReader(customAutomatorsSource));
}
// Otherwise we'll see if there a file on the SD Card
else {
File sdCard = Environment.getExternalStorageDirectory();
if (sdCard == null || !sdCard.exists())
return;
File customAutomators = new File(sdCard, "FoneMonkeyAutomators.txt");
if (!customAutomators.exists())
return;
Log.log("Processing automators in: " + customAutomators);
in = new BufferedReader(new FileReader(customAutomators));
}
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.startsWith("#"))
continue;
if (line.startsWith("%")) {
line = line.toUpperCase();
if (line.equals("%CLEAR")) {
automators.clear();
automatorsByType.clear();
} else if (line.equals("%NO_OVERRIDES"))
noOverrides = true;
continue;
}
Class<?> klass = Class.forName(line);
IAutomator ia = (IAutomator) klass.newInstance();
if (noOverrides && automatorsByType.get(ia.getComponentType()) != null) {
Log.log("Warning: Overriding automator type \"" + ia.getComponentType()
+ "\" not allowed due to %NO_OVERRIDES directive.");
continue;
}
registerClass(ia.getComponentType(), ia.getComponentClass(), klass);
}
in.close();
}
private static HashMap<String, Class<?>> automators = new HashMap<String, Class<?>>();
private static HashMap<String, Class<?>> automatorsByType = new HashMap<String, Class<?>>();
private static HashMap<String, Class<?>> htmlAutomatorsByType = new HashMap<String, Class<?>>();
private static HashMap<Integer, IAutomator> automatorCache = new HashMap<Integer, IAutomator>();
public static void registerClass(String componentType, Class<?> componentClass,
Class<?> automatorClass) {
registerClass(componentType, componentClass, automatorClass, null);
}
public static void registerClass(String componentType, Class<?> componentClass,
Class<?> automatorClass, String[] aliases) {
registerClass(componentType, componentClass, automatorClass, aliases, false);
}
public static void registerClass(String componentType, Class<?> componentClass,
Class<?> automatorClass, String[] aliases, boolean isHtmlAutomator) {
if (componentClass != null) {
automators.put(componentClass.getName(), automatorClass);
}
if (isHtmlAutomator) {
htmlAutomatorsByType.put(componentType, automatorClass);
} else {
automatorsByType.put(componentType, automatorClass);
}
if (aliases == null) {
return;
}
for (String alias : aliases) {
if (isHtmlAutomator) {
htmlAutomatorsByType.put(alias, automatorClass);
} else {
automatorsByType.put(alias, automatorClass);
}
}
}
private static int foundSoFar = 0;
private static Pattern arrayPattern = Pattern.compile("^(.+?)\\((\\d+)\\)$");
/**
* Return the view with the specified componentType and monkeyID. Matches on componentType only
* if monkeyID is blank or null.
*
* @param componentType
* Type type of the component to find
* @param monkeyID
* If blank or null, the (indeterminate) first component found with a matching
* componentType will be returned
* @return the found View or HtmlElement, or null if no View can be found
*/
public static Object findComponentByMonkeyID(String componentType, String monkeyID) {
Class<?> type = getAutomator(componentType);
if (type == null) {
throw new IllegalArgumentException("Unrecognized component type: " + componentType);
}
String id = monkeyID;
int index = 1;
foundSoFar = 0;
Matcher matcher = arrayPattern.matcher(monkeyID);
if (matcher.matches()) {
// matcher.find();
id = matcher.group(1);
index = Integer.valueOf(matcher.group(2));
}
HashSet<View> roots = new HashSet<View>(getRoots());
Dialog box = ActivityManager.getCurrentDialog();
if (box != null) {
roots.add(box.getWindow().getDecorView());
}
for (View root : roots) {
if (!root.isShown())
continue;
Object v = _findComponentByMonkeyID(root, componentType, id, index);
if (v != null) {
return v;
}
}
return null;
}
public static Set<View> getRoots() {
return FunctionalityAdder.getRoots();
}
private static Object _findComponentByMonkeyID(View root, String componentType,
String monkeyID, int index) {
IAutomator automator = AutomationManager.findAutomator(root);
if (automator == null) {
Log.log("Unable to find an automator for " + root.getClass().getName());
return null;
}
if (automator instanceof WebViewAutomator && root.isShown()) {
if (automator.canAutomate(componentType, monkeyID)) {
// The command is being directed to the webview itself
return root;
}
// Need to deal with ordinals on webview
HtmlElement elem = ((WebViewAutomator) automator).findHtmlElement(componentType,
monkeyID, index);
if (elem != null) {
return elem;
} else {
return null;
}
}
if (automator.canAutomate(componentType, monkeyID)) {
foundSoFar++;
if (foundSoFar == index) {
return root;
}
}
if (!(root instanceof ViewGroup)) {
return null;
}
ViewGroup vg = (ViewGroup) root;
for (int i = 0; i < vg.getChildCount(); ++i) {
Object c = _findComponentByMonkeyID(vg.getChildAt(i), componentType, monkeyID, index);
if (c != null) {
return c;
}
}
return null;
}
private static List<Object> filterComponentsForWildcardMonkeyId(String componentType,
String monkeyId) {
List<Object> views = new ArrayList<Object>();
Set<View> roots = new HashSet<View>(getRoots());
Dialog box = ActivityManager.getCurrentDialog();
if (box != null) {
roots.add(box.getWindow().getDecorView());
}
for (View root : roots) {
if (!root.isShown()) {
continue;
}
views.addAll(_findComponentsForWildcardMonkeyId(root, componentType, monkeyId));
}
return views;
}
private static List<Object> _findComponentsForWildcardMonkeyId(View root, String componentType,
String monkeyId) {
List<Object> views = new ArrayList<Object>();
IAutomator automator = AutomationManager.findAutomator(root);
if (automator == null) {
Log.log("Unable to find an automator for " + root.getClass().getName());
return views;
}
if (matchesWildcardMonkeyId(automator, componentType, monkeyId)) {
Log.log("found " + automator.toString());
views.add(root);
}
/*
* JUSTIN: stupid webview wildcard monkeyId hack... Just do the component lookup inside the
* webview for non-wildcard monkeyId and add it as a view
*/
if (monkeyId.equals("*") && automator instanceof WebViewAutomator && root.isShown()) {
Object view = findComponentByMonkeyID(componentType, monkeyId);
views.add(view);
}
if (root instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) root;
for (int i = 0; i < vg.getChildCount(); i++) {
View child = vg.getChildAt(i);
views.addAll(_findComponentsForWildcardMonkeyId(child, componentType, monkeyId));
}
}
return views;
}
private static boolean matchesWildcardMonkeyId(IAutomator automator, String componentType,
String monkeyId) {
if (automator instanceof ViewAutomator && ((ViewAutomator) automator).getView().isShown()
&& automator.forSubtypeOf(componentType)) {
return PropertyUtil.wildcardMatch(monkeyId, automator.getMonkeyID());
}
return false;
}
public static List<IAutomator> findAllWildcardMonkeyIdAutomators(String componentType,
String monkeyId) {
List<IAutomator> filteredAutomators = new ArrayList<IAutomator>();
Class<?> c = getAutomator(componentType);
if (c == null) {
throw new IllegalArgumentException("Unrecognized component type: " + componentType);
}
List<Object> views = filterComponentsForWildcardMonkeyId(componentType, monkeyId);
for (Object view : views) {
IAutomator automator = null;
try {
automator = (IAutomator) c.newInstance();
} catch (Exception e) {
Log.log("Failure instancing automator for type " + componentType + ": "
+ e.getMessage());
}
if (automator == null) {
continue;
}
Class<?> k = automator.getComponentClass();
if (k != null
&& (View.class.isAssignableFrom(k) || HtmlElement.class.isAssignableFrom(k))) {
// UI View or Html element. Find the corresponding component in the UI Tree.
if (componentType.equals("")) {
componentType = ViewAutomator.componentType;
}
if (view == null) {
continue;
}
if (view.getClass() == HtmlElement.class) {
Class<?> htmlAutomatorClass = getHtmlAutomator(componentType);
if (htmlAutomatorClass == null) {
htmlAutomatorClass = HtmlElementAutomator.class;
}
try {
automator = (IAutomator) htmlAutomatorClass.newInstance();
} catch (Exception ex) {
// ignore
}
}
if (view.getClass() != automator.getComponentClass()) {
// ComponentType was for a supertype so there might be exist an
// automator subclass
automator = AutomationManager.findAutomator(view);
}
automator.setComponent(view);
} else {
// Non-UI component
automator.setMonkeyID(monkeyId);
}
filteredAutomators.add(automator);
}
return filteredAutomators;
}
public static IAutomator find(String componentType, String monkeyID) {
return find(componentType, monkeyID, false);
}
public static IAutomator find(String componentType, String monkeyID, boolean nullOnNotFound) {
Class<?> c = getAutomator(componentType);
if (c == null) {
throw new IllegalArgumentException("Unrecognized component type: " + componentType);
}
IAutomator automator = null;
try {
automator = (IAutomator) c.newInstance();
} catch (Exception e) {
Log.log("Failure instancing automator for type " + componentType + ": "
+ e.getMessage());
}
if (automator == null) {
return null;
}
Class<?> k = automator.getComponentClass();
if (k != null && (View.class.isAssignableFrom(k) || HtmlElement.class.isAssignableFrom(k))) {
// UI View or Html element. Find the corresponding component in the UI Tree.
if (componentType.equals("")) {
componentType = ViewAutomator.componentType;
}
Object view = findComponentByMonkeyID(componentType, monkeyID);
if (view == null) {
if (nullOnNotFound) {
return null;
}
throw new FoneMonkeyScriptFailure("Unable to find " + componentType
+ " with monkeyID \"" + monkeyID + "\"");
}
if (view.getClass() == HtmlElement.class) {
Class<?> htmlAutomatorClass = getHtmlAutomator(componentType);
if (htmlAutomatorClass == null) {
htmlAutomatorClass = HtmlElementAutomator.class;
}
try {
automator = (IAutomator) htmlAutomatorClass.newInstance();
} catch (Exception e) {
Log.log("Failure instancing automator for type " + componentType + ": "
+ e.getMessage());
}
}
if (view.getClass() != automator.getComponentClass()) {
// ComponentType was for a supertype so there might be exist an
// automator subclass
automator = AutomationManager.findAutomator(view);
}
automator.setComponent(view);
} else {
// Non-UI component
automator.setMonkeyID(monkeyID);
}
return automator;
}
/**
* Find an automator that handles the class of this object. If none exists then return the
* automator that handles the nearest superclass. If o is null, return DeviceAutomator (ewww),
* otherwise return null.
*/
public static IAutomator findAutomator(Object o) {
if (o == null) {
return findAutomatorByType(AutomatorConstants.TYPE_DEVICE);
}
// JUSTIN: let's clear the cache before each search, so we can always start fresh
automatorCache.clear();
// get the automator for the given view's class
return _findAutomator(o, o.getClass());
}
/**
* Find the automator for the given non-null object (typically a View). If none exists, then
* return the automator for the nearest superclass. If none can be found, return null.
*/
private static IAutomator _findAutomator(Object o, Class<?> c) {
// first, check the cache
IAutomator cached = automatorCache.get(o.hashCode());
if (cached != null) {
return cached;
}
// next, just get the automator for the given class
Class<?> klass = automators.get(c.getName());
// if that doesn't work, try the interfaces of the view
if (klass == null) {
for (Class<?> k : o.getClass().getInterfaces()) {
klass = automators.get(k.getName());
if (klass != null) {
break;
}
}
}
// if we found it, then instantiate and cache
IAutomator automator = null;
if (klass != null) {
try {
automator = (IAutomator) klass.newInstance();
} catch (Exception e) {
Log.log("Error instancing automator " + o.getClass().getName());
}
if (automator != null) {
automator.setComponent(o);
automatorCache.put(o.hashCode(), automator);
return automator;
}
}
// nothing found yet, try the superclass
Class<?> parent = c.getSuperclass();
return (parent == null ? null : _findAutomator(o, parent));
}
public static IAutomator findAutomatorByType(String type) {
Class<?> klass = automatorsByType.get(type);
IAutomator automator = null;
if (klass != null) {
try {
automator = (IAutomator) klass.newInstance();
} catch (Exception e) {
Log.log("Error instancing automator for type " + type);
}
return automator;
}
return null;
}
private static int index = 0, found = 0;
public static String findIndexedMonkeyIdIfAny(IAutomator automator) {
if (automator.getOrdinal() > 1) {
index = 0;
found = 0;
findIndex(automator.getComponentType(), automator.getMonkeyID(), automator.getOrdinal());
if (index > 1) {
return automator.getMonkeyID() + "(" + index + ")";
}
}
return automator.getMonkeyID();
}
private static Object findIndex(String componentType, String monkeyID, int ordinal) {
Set<View> roots = new HashSet<View>(getRoots());
Dialog box = ActivityManager.getCurrentDialog();
if (box != null) {
roots.add(box.getWindow().getDecorView());
}
for (View root : roots) {
if (!root.isShown()) {
continue;
}
Object c = _findIndex(root, componentType, monkeyID, ordinal);
if (c != null) {
return c;
}
}
return null;
}
private static Object _findIndex(View root, String componentType, String monkeyID, int ordinal) {
if (root instanceof View) {
IAutomator automator = AutomationManager.findAutomatorByType(componentType);
if (automator == null) {
return null;
}
if (root.getClass() == automator.getComponentClass()) {
automator.setComponent(root);
found++;
if (automator.getMonkeyID().equals(monkeyID)) {
index++;
}
if (found == ordinal) {
return root;
}
}
}
if (root instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) root;
for (int i = 0; i < vg.getChildCount(); ++i) {
Object c = _findIndex(vg.getChildAt(i), componentType, monkeyID, ordinal);
if (c != null) {
return index;
}
}
}
return null;
}
public static boolean record(String action, Object view, String... args) {
IAutomator automator = AutomationManager.findAutomator(view);
if (automator == null) {
return false;
}
if (shouldIgnore(automator, action)) {
cancelIgnore();
return true;
}
automator.record(action, args);
return true;
}
private static boolean shouldIgnore(IAutomator automator, String action) {
Object view = automator.getComponent();
if (view instanceof View) {
View v = (View) view;
if (isHiddenByParent(v)) {
return true;
}
if (ignoreNextAction != null && ignoreNextAction.equalsIgnoreCase(action)
&& ignoreNextView == v) {
return true;
}
// ignore components in webviews (if view is null, it is a hardware button)
if (v.getParent().getClass() == WebView.class || v.getClass() == WebView.class) {
return true;
}
}
return false;
}
// This needs to get folded into generic record
public static boolean recordTab(String operation, String... args) {
Class<?> klass = automatorsByType.get(AutomatorConstants.TYPE_TABBAR);
if (klass == null)
return false;
IAutomator automator;
try {
automator = (IAutomator) klass.newInstance();
} catch (Exception e) {
Log.log("Error instancing automator " + klass.getName());
return false;
}
automator.record(operation, args);
return true;
}
/**
* @param runnable
*/
public static void runOnUIThread(Runnable runnable) {
AutomationManager.getTopActivity().runOnUiThread(runnable);
}
/**
* Traverse ancestors to see if any are hiding this component from automation
*
* @param view
* the view to be checked
* @return true if any ancestor is hiding this view
*/
public static boolean isHiddenByParent(View view) {
// Log.log("Checking if " + view + " is hidden");
return _isHiddenByParent(view, view.getParent());
}
private static boolean _isHiddenByParent(View view, ViewParent parent) {
// Log.log("Checking if " + view + " is hidden by " + parent);
if (parent == null) {
// Log.log(view + " is not hidden");
return false;
}
IAutomator automator = AutomationManager.findAutomator(parent);
if (automator == null) {
return false;
}
if (automator.hides(view)) {
return true;
}
// Log.log("Found automator " + automator);
return (automator == null) ? false : _isHiddenByParent(view, parent.getParent());
}
/**
*
* Use this method to register a Non-UI component (eg, Device).
*
* @param componentType
* @param class1
*
*/
public static void registerClass(String componentType, Class<?> automator) {
registerClass(componentType, null, automator);
}
/**
*
* the activity currently on top of the stack
*/
public static Activity getTopActivity() {
ComponentName topDog = ((android.app.ActivityManager) Recorder.getSomeActivity()
.getSystemService(Application.ACTIVITY_SERVICE)).getRunningTasks(1).get(0).topActivity;
Activity topActivity = ActivityManager.getActivity(topDog.flattenToString());
if (topActivity==null) {
System.out.println("null top activity :-(");
System.out.println(" top dog: " + topDog);
System.out.println(" top dog.flattenToString(): " + topDog.flattenToString());
System.out.println(" Recorder.getSomeActivity(): " + Recorder.getSomeActivity());
}
return topActivity;
}
public static String dumpViewTree() {
JSONObject root = new JSONObject();
try {
root.put("ComponentType", "root");
root.put("monkeyId", "");
root.put("className", "");
root.put("visible", "true");
root.put("identifiers", new JSONArray());
root.put("ordinal", 1);
JSONArray kids = new JSONArray();
Set<View> strippedRoots = FunctionalityAdder.getRootsStripped();
for (View view : strippedRoots) {
JSONObject comp = getJson(view);
kids.put(comp);
}
root.put("children", kids);
} catch (JSONException e) {
e.printStackTrace();
}
return root.toString();
}
private static JSONObject getJson(View view) {
IAutomator automator = findAutomator(view);
if (automator == null) {
IllegalStateException ex = new IllegalStateException("Unable to find automator for "
+ view.getClass().getName());
Log.log("Error dumping tree", ex);
throw ex;
}
JSONObject comp = new JSONObject();
try {
comp.put("ComponentType", automator.getComponentType());
comp.put("monkeyId", findIndexedMonkeyIdIfAny(automator));
comp.put("className", view.getClass().getName());
comp.put("visible", String.valueOf(view.isShown()));
comp.put("identifiers", new JSONArray(automator.getIdentifyingValues()));
comp.put("ordinal", automator.getOrdinal());
JSONArray kids;
if (automator instanceof WebViewAutomator) {
kids = getJsonForWebView((WebViewAutomator) automator);
} else {
kids = getJsonForKids(view);
}
comp.put("children", kids);
} catch (Exception e) {
e.printStackTrace();
}
return comp;
}
private static JSONArray getJsonForWebView(WebViewAutomator automator) throws JSONException {
String componentTreeJson = automator.getComponentTreeJson();
if (componentTreeJson != null && componentTreeJson.length() > 0) {
return new JSONArray(componentTreeJson);
}
return new JSONArray();
}
private static JSONArray getJsonForKids(View view) {
JSONArray kids = new JSONArray();
if (!(view instanceof ViewGroup)) {
return kids;
}
ViewGroup vg = (ViewGroup) view;
for (int i = 0; i < vg.getChildCount(); ++i) {
kids.put(getJson(vg.getChildAt(i)));
}
return kids;
}
private static Class<?> getAutomator(String type) {
Class<?> c;
if (type.equals("") || type.equals("*")) {
c = automatorsByType.get(AutomatorConstants.TYPE_VIEW);
} else {
c = automatorsByType.get(type);
}
if (c == null) {
c = getHtmlAutomator(type);
}
return c;
}
private static Class<?> getHtmlAutomator(String type) {
Class<?> c;
if (type.equals("") || type.equals("*")) {
c = htmlAutomatorsByType.get(AutomatorConstants.TYPE_VIEW);
} else {
c = htmlAutomatorsByType.get(type);
}
return c;
}
/**
* Call to suppress recording an event resulting from a programmatic (rather than UI) action
*
* @param view
* @param action
*/
public static void ignoreNext(View view, String action) {
ignoreNextView = view;
ignoreNextAction = action;
}
public static void cancelIgnore() {
ignoreNextView = null;
ignoreNextAction = null;
}
}