/*
* Copyright 2011, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 com.android.commands.monkey;
import static com.android.commands.monkey.MonkeySourceNetwork.EARG;
import android.accessibilityservice.UiTestAutomationBridge;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.commands.monkey.MonkeySourceNetwork.CommandQueue;
import com.android.commands.monkey.MonkeySourceNetwork.MonkeyCommand;
import com.android.commands.monkey.MonkeySourceNetwork.MonkeyCommandReturn;
import dalvik.system.DexClassLoader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class that enables Monkey to perform view introspection when issued Monkey Network
* Script commands over the network.
*/
public class MonkeySourceNetworkViews {
protected static UiTestAutomationBridge sUiTestAutomationBridge;
private static IPackageManager sPm =
IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
private static Map<String, Class<?>> sClassMap = new HashMap<String, Class<?>>();
private static final String REMOTE_ERROR =
"Unable to retrieve application info from PackageManager";
private static final String CLASS_NOT_FOUND = "Error retrieving class information";
private static final String NO_ACCESSIBILITY_EVENT = "No accessibility event has occured yet";
private static final String NO_NODE = "Node with given ID does not exist";
private static final String NO_CONNECTION = "Failed to connect to AccessibilityService, "
+ "try restarting Monkey";
private static final Map<String, ViewIntrospectionCommand> COMMAND_MAP =
new HashMap<String, ViewIntrospectionCommand>();
/* Interface for view queries */
private static interface ViewIntrospectionCommand {
/**
* Get the response to the query
* @return the response to the query
*/
public MonkeyCommandReturn query(AccessibilityNodeInfo node, List<String> args);
}
static {
COMMAND_MAP.put("getlocation", new GetLocation());
COMMAND_MAP.put("gettext", new GetText());
COMMAND_MAP.put("getclass", new GetClass());
COMMAND_MAP.put("getchecked", new GetChecked());
COMMAND_MAP.put("getenabled", new GetEnabled());
COMMAND_MAP.put("getselected", new GetSelected());
COMMAND_MAP.put("setselected", new SetSelected());
COMMAND_MAP.put("getfocused", new GetFocused());
COMMAND_MAP.put("setfocused", new SetFocused());
COMMAND_MAP.put("getparent", new GetParent());
COMMAND_MAP.put("getchildren", new GetChildren());
COMMAND_MAP.put("getaccessibilityids", new GetAccessibilityIds());
}
/**
* Registers the event listener for AccessibilityEvents.
* Also sets up a communication connection so we can query the
* accessibility service.
*/
public static void setup() {
sUiTestAutomationBridge = new UiTestAutomationBridge();
sUiTestAutomationBridge.connect();
}
/**
* Get the ID class for the given package.
* This will cause issues if people reload a package with different
* resource identifiers, but don't restart the Monkey server.
*
* @param packageName The package that we want to retrieve the ID class for
* @return The ID class for the given package
*/
private static Class<?> getIdClass(String packageName, String sourceDir)
throws ClassNotFoundException {
// This kind of reflection is expensive, so let's only do it
// if we need to
Class<?> klass = sClassMap.get(packageName);
if (klass == null) {
DexClassLoader classLoader = new DexClassLoader(
sourceDir, "/data/local/tmp",
null, ClassLoader.getSystemClassLoader());
klass = classLoader.loadClass(packageName + ".R$id");
sClassMap.put(packageName, klass);
}
return klass;
}
private static String getPositionFromNode(AccessibilityNodeInfo node) {
Rect nodePosition = new Rect();
node.getBoundsInScreen(nodePosition);
StringBuilder positions = new StringBuilder();
positions.append(nodePosition.left).append(" ").append(nodePosition.top);
positions.append(" ").append(nodePosition.right-nodePosition.left).append(" ");
positions.append(nodePosition.bottom-nodePosition.top);
return positions.toString();
}
/**
* Converts a resource identifier into it's generated integer ID
*
* @param stringId the string identifier
* @return the generated integer identifier.
*/
private static int getId(String stringId, AccessibilityEvent event)
throws MonkeyViewException {
try {
AccessibilityNodeInfo node = event.getSource();
String packageName = node.getPackageName().toString();
ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0, UserHandle.myUserId());
Class<?> klass;
klass = getIdClass(packageName, appInfo.sourceDir);
return klass.getField(stringId).getInt(null);
} catch (RemoteException e) {
throw new MonkeyViewException(REMOTE_ERROR);
} catch (ClassNotFoundException e){
throw new MonkeyViewException(e.getMessage());
} catch (NoSuchFieldException e){
throw new MonkeyViewException("No such node with given id");
} catch (IllegalAccessException e){
throw new MonkeyViewException("Private identifier");
} catch (NullPointerException e) {
// AccessibilityServiceConnection throws a NullPointerException if you hand it
// an ID that doesn't exist onscreen
throw new MonkeyViewException("No node with given id exists onscreen");
}
}
private static AccessibilityNodeInfo getNodeByAccessibilityIds(
String windowString, String viewString) {
int windowId = Integer.parseInt(windowString);
int viewId = Integer.parseInt(viewString);
return sUiTestAutomationBridge.findAccessibilityNodeInfoByAccessibilityId(windowId,
viewId);
}
private static AccessibilityNodeInfo getNodeByViewId(String viewId, AccessibilityEvent event)
throws MonkeyViewException {
int id = getId(viewId, event);
return sUiTestAutomationBridge.findAccessibilityNodeInfoByViewId(
UiTestAutomationBridge.ACTIVE_WINDOW_ID, UiTestAutomationBridge.ROOT_NODE_ID, id);
}
/**
* Command to list all possible view ids for the given application.
* This lists all view ids regardless if they are on screen or not.
*/
public static class ListViewsCommand implements MonkeyCommand {
//listviews
public MonkeyCommandReturn translateCommand(List<String> command,
CommandQueue queue) {
AccessibilityEvent lastEvent = sUiTestAutomationBridge.getLastAccessibilityEvent();
if (lastEvent == null) {
return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
}
AccessibilityNodeInfo node = lastEvent.getSource();
/* Occasionally the API will generate an event with no source, which is essentially the
* same as it generating no event at all */
if (node == null) {
return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
}
String packageName = node.getPackageName().toString();
try{
Class<?> klass;
ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0, UserHandle.myUserId());
klass = getIdClass(packageName, appInfo.sourceDir);
StringBuilder fieldBuilder = new StringBuilder();
Field[] fields = klass.getFields();
for (Field field : fields) {
fieldBuilder.append(field.getName() + " ");
}
return new MonkeyCommandReturn(true, fieldBuilder.toString());
} catch (RemoteException e){
return new MonkeyCommandReturn(false, REMOTE_ERROR);
} catch (ClassNotFoundException e){
return new MonkeyCommandReturn(false, CLASS_NOT_FOUND);
}
}
}
/**
* A command that allows for querying of views. It takes an id type, the requisite ids,
* and the command for querying the view.
*/
public static class QueryViewCommand implements MonkeyCommand {
//queryview [id type] [id(s)] [command]
//queryview viewid button1 gettext
//queryview accessibilityids 12 5 getparent
public MonkeyCommandReturn translateCommand(List<String> command,
CommandQueue queue) {
if (command.size() > 2) {
if (!sUiTestAutomationBridge.isConnected()) {
return new MonkeyCommandReturn(false, NO_CONNECTION);
}
AccessibilityEvent lastEvent = sUiTestAutomationBridge.getLastAccessibilityEvent();
if (lastEvent == null) {
return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
}
String idType = command.get(1);
AccessibilityNodeInfo node;
String viewQuery;
List<String> args;
if ("viewid".equals(idType)) {
try {
node = getNodeByViewId(command.get(2), lastEvent);
viewQuery = command.get(3);
args = command.subList(4, command.size());
} catch (MonkeyViewException e) {
return new MonkeyCommandReturn(false, e.getMessage());
}
} else if (idType.equals("accessibilityids")) {
try {
node = getNodeByAccessibilityIds(command.get(2), command.get(3));
viewQuery = command.get(4);
args = command.subList(5, command.size());
} catch (NumberFormatException e) {
return EARG;
}
} else {
return EARG;
}
if (node == null) {
return new MonkeyCommandReturn(false, NO_NODE);
}
ViewIntrospectionCommand getter = COMMAND_MAP.get(viewQuery);
if (getter != null) {
return getter.query(node, args);
} else {
return EARG;
}
}
return EARG;
}
}
/**
* A command that returns the accessibility ids of the root view.
*/
public static class GetRootViewCommand implements MonkeyCommand {
// getrootview
public MonkeyCommandReturn translateCommand(List<String> command,
CommandQueue queue) {
AccessibilityEvent lastEvent = sUiTestAutomationBridge.getLastAccessibilityEvent();
if (lastEvent == null) {
return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
}
AccessibilityNodeInfo node = lastEvent.getSource();
return (new GetAccessibilityIds()).query(node, new ArrayList<String>());
}
}
/**
* A command that returns the accessibility ids of the views that contain the given text.
* It takes a string of text and returns the accessibility ids of the nodes that contain the
* text as a list of integers separated by spaces.
*/
public static class GetViewsWithTextCommand implements MonkeyCommand {
// getviewswithtext [text]
// getviewswithtext "some text here"
public MonkeyCommandReturn translateCommand(List<String> command,
CommandQueue queue) {
if (!sUiTestAutomationBridge.isConnected()) {
return new MonkeyCommandReturn(false, NO_CONNECTION);
}
if (command.size() == 2) {
String text = command.get(1);
List<AccessibilityNodeInfo> nodes = sUiTestAutomationBridge
.findAccessibilityNodeInfosByText(UiTestAutomationBridge.ACTIVE_WINDOW_ID,
UiTestAutomationBridge.ROOT_NODE_ID, text);
ViewIntrospectionCommand idGetter = new GetAccessibilityIds();
List<String> emptyArgs = new ArrayList<String>();
StringBuilder ids = new StringBuilder();
for (AccessibilityNodeInfo node : nodes) {
MonkeyCommandReturn result = idGetter.query(node, emptyArgs);
if (!result.wasSuccessful()){
return result;
}
ids.append(result.getMessage()).append(" ");
}
return new MonkeyCommandReturn(true, ids.toString());
}
return EARG;
}
}
/**
* Command to retrieve the location of the given node.
* Returns the x, y, width and height of the view, separated by spaces.
*/
public static class GetLocation implements ViewIntrospectionCommand {
//queryview [id type] [id] getlocation
//queryview viewid button1 getlocation
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
Rect nodePosition = new Rect();
node.getBoundsInScreen(nodePosition);
StringBuilder positions = new StringBuilder();
positions.append(nodePosition.left).append(" ").append(nodePosition.top);
positions.append(" ").append(nodePosition.right-nodePosition.left).append(" ");
positions.append(nodePosition.bottom-nodePosition.top);
return new MonkeyCommandReturn(true, positions.toString());
}
return EARG;
}
}
/**
* Command to retrieve the text of the given node
*/
public static class GetText implements ViewIntrospectionCommand {
//queryview [id type] [id] gettext
//queryview viewid button1 gettext
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
if (node.isPassword()){
return new MonkeyCommandReturn(false, "Node contains a password");
}
/* Occasionally we get a null from the accessibility API, rather than an empty
* string */
if (node.getText() == null) {
return new MonkeyCommandReturn(true, "");
}
return new MonkeyCommandReturn(true, node.getText().toString());
}
return EARG;
}
}
/**
* Command to retrieve the class name of the given node
*/
public static class GetClass implements ViewIntrospectionCommand {
//queryview [id type] [id] getclass
//queryview viewid button1 getclass
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
return new MonkeyCommandReturn(true, node.getClassName().toString());
}
return EARG;
}
}
/**
* Command to retrieve the checked status of the given node
*/
public static class GetChecked implements ViewIntrospectionCommand {
//queryview [id type] [id] getchecked
//queryview viewid button1 getchecked
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
return new MonkeyCommandReturn(true, Boolean.toString(node.isChecked()));
}
return EARG;
}
}
/**
* Command to retrieve whether the given node is enabled
*/
public static class GetEnabled implements ViewIntrospectionCommand {
//queryview [id type] [id] getenabled
//queryview viewid button1 getenabled
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
return new MonkeyCommandReturn(true, Boolean.toString(node.isEnabled()));
}
return EARG;
}
}
/**
* Command to retrieve whether the given node is selected
*/
public static class GetSelected implements ViewIntrospectionCommand {
//queryview [id type] [id] getselected
//queryview viewid button1 getselected
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
return new MonkeyCommandReturn(true, Boolean.toString(node.isSelected()));
}
return EARG;
}
}
/**
* Command to set the selected status of the given node. Takes a boolean value as its only
* argument.
*/
public static class SetSelected implements ViewIntrospectionCommand {
//queryview [id type] [id] setselected [boolean]
//queryview viewid button1 setselected true
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 1) {
boolean actionPerformed;
if (Boolean.valueOf(args.get(0))) {
actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_SELECT);
} else if (!Boolean.valueOf(args.get(0))) {
actionPerformed =
node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
} else {
return EARG;
}
return new MonkeyCommandReturn(actionPerformed);
}
return EARG;
}
}
/**
* Command to get whether the given node is focused.
*/
public static class GetFocused implements ViewIntrospectionCommand {
//queryview [id type] [id] getfocused
//queryview viewid button1 getfocused
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
return new MonkeyCommandReturn(true, Boolean.toString(node.isFocused()));
}
return EARG;
}
}
/**
* Command to set the focus status of the given node. Takes a boolean value
* as its only argument.
*/
public static class SetFocused implements ViewIntrospectionCommand {
//queryview [id type] [id] setfocused [boolean]
//queryview viewid button1 setfocused false
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 1) {
boolean actionPerformed;
if (Boolean.valueOf(args.get(0))) {
actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
} else if (!Boolean.valueOf(args.get(0))) {
actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
} else {
return EARG;
}
return new MonkeyCommandReturn(actionPerformed);
}
return EARG;
}
}
/**
* Command to get the accessibility ids of the given node. Returns the accessibility ids as a
* space separated pair of integers with window id coming first, followed by the accessibility
* view id.
*/
public static class GetAccessibilityIds implements ViewIntrospectionCommand {
//queryview [id type] [id] getaccessibilityids
//queryview viewid button1 getaccessibilityids
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
int viewId;
try {
Class<?> klass = node.getClass();
Field field = klass.getDeclaredField("mAccessibilityViewId");
field.setAccessible(true);
viewId = ((Integer) field.get(node)).intValue();
} catch (NoSuchFieldException e) {
return new MonkeyCommandReturn(false, NO_NODE);
} catch (IllegalAccessException e) {
return new MonkeyCommandReturn(false, "Access exception");
}
String ids = node.getWindowId() + " " + viewId;
return new MonkeyCommandReturn(true, ids);
}
return EARG;
}
}
/**
* Command to get the accessibility ids of the parent of the given node. Returns the
* accessibility ids as a space separated pair of integers with window id coming first followed
* by the accessibility view id.
*/
public static class GetParent implements ViewIntrospectionCommand {
//queryview [id type] [id] getparent
//queryview viewid button1 getparent
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
AccessibilityNodeInfo parent = node.getParent();
if (parent == null) {
return new MonkeyCommandReturn(false, "Given node has no parent");
}
return (new GetAccessibilityIds()).query(parent, new ArrayList<String>());
}
return EARG;
}
}
/**
* Command to get the accessibility ids of the children of the given node. Returns the
* children's ids as a space separated list of integer pairs. Each of the pairs consists of the
* window id, followed by the accessibility id.
*/
public static class GetChildren implements ViewIntrospectionCommand {
//queryview [id type] [id] getchildren
//queryview viewid button1 getchildren
public MonkeyCommandReturn query(AccessibilityNodeInfo node,
List<String> args) {
if (args.size() == 0) {
ViewIntrospectionCommand idGetter = new GetAccessibilityIds();
List<String> emptyArgs = new ArrayList<String>();
StringBuilder ids = new StringBuilder();
int totalChildren = node.getChildCount();
for (int i = 0; i < totalChildren; i++) {
MonkeyCommandReturn result = idGetter.query(node.getChild(i), emptyArgs);
if (!result.wasSuccessful()) {
return result;
} else {
ids.append(result.getMessage()).append(" ");
}
}
return new MonkeyCommandReturn(true, ids.toString());
}
return EARG;
}
}
}