Home | History | Annotate | Download | only in monkey
      1 /*
      2  * Copyright 2011, The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.commands.monkey;
     18 
     19 import static com.android.commands.monkey.MonkeySourceNetwork.EARG;
     20 
     21 import android.accessibilityservice.UiTestAutomationBridge;
     22 import android.content.pm.ApplicationInfo;
     23 import android.content.pm.IPackageManager;
     24 import android.graphics.Rect;
     25 import android.os.RemoteException;
     26 import android.os.ServiceManager;
     27 import android.os.UserId;
     28 import android.view.accessibility.AccessibilityEvent;
     29 import android.view.accessibility.AccessibilityNodeInfo;
     30 
     31 import com.android.commands.monkey.MonkeySourceNetwork.CommandQueue;
     32 import com.android.commands.monkey.MonkeySourceNetwork.MonkeyCommand;
     33 import com.android.commands.monkey.MonkeySourceNetwork.MonkeyCommandReturn;
     34 
     35 import dalvik.system.DexClassLoader;
     36 
     37 import java.lang.reflect.Field;
     38 import java.util.ArrayList;
     39 import java.util.HashMap;
     40 import java.util.List;
     41 import java.util.Map;
     42 
     43 /**
     44  * Utility class that enables Monkey to perform view introspection when issued Monkey Network
     45  * Script commands over the network.
     46  */
     47 public class MonkeySourceNetworkViews {
     48     protected static UiTestAutomationBridge sUiTestAutomationBridge;
     49     private static IPackageManager sPm =
     50             IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
     51     private static Map<String, Class<?>> sClassMap = new HashMap<String, Class<?>>();
     52 
     53     private static final String REMOTE_ERROR =
     54             "Unable to retrieve application info from PackageManager";
     55     private static final String CLASS_NOT_FOUND = "Error retrieving class information";
     56     private static final String NO_ACCESSIBILITY_EVENT = "No accessibility event has occured yet";
     57     private static final String NO_NODE = "Node with given ID does not exist";
     58     private static final String NO_CONNECTION = "Failed to connect to AccessibilityService, "
     59                                                 + "try restarting Monkey";
     60 
     61     private static final Map<String, ViewIntrospectionCommand> COMMAND_MAP =
     62             new HashMap<String, ViewIntrospectionCommand>();
     63 
     64     /* Interface for view queries */
     65     private static interface ViewIntrospectionCommand {
     66         /**
     67          * Get the response to the query
     68          * @return the response to the query
     69          */
     70         public MonkeyCommandReturn query(AccessibilityNodeInfo node, List<String> args);
     71     }
     72 
     73     static {
     74         COMMAND_MAP.put("getlocation", new GetLocation());
     75         COMMAND_MAP.put("gettext", new GetText());
     76         COMMAND_MAP.put("getclass", new GetClass());
     77         COMMAND_MAP.put("getchecked", new GetChecked());
     78         COMMAND_MAP.put("getenabled", new GetEnabled());
     79         COMMAND_MAP.put("getselected", new GetSelected());
     80         COMMAND_MAP.put("setselected", new SetSelected());
     81         COMMAND_MAP.put("getfocused", new GetFocused());
     82         COMMAND_MAP.put("setfocused", new SetFocused());
     83         COMMAND_MAP.put("getparent", new GetParent());
     84         COMMAND_MAP.put("getchildren", new GetChildren());
     85         COMMAND_MAP.put("getaccessibilityids", new GetAccessibilityIds());
     86     }
     87 
     88     /**
     89      * Registers the event listener for AccessibilityEvents.
     90      * Also sets up a communication connection so we can query the
     91      * accessibility service.
     92      */
     93     public static void setup() {
     94         sUiTestAutomationBridge = new UiTestAutomationBridge();
     95         sUiTestAutomationBridge.connect();
     96     }
     97 
     98     /**
     99      * Get the ID class for the given package.
    100      * This will cause issues if people reload a package with different
    101      * resource identifiers, but don't restart the Monkey server.
    102      *
    103      * @param packageName The package that we want to retrieve the ID class for
    104      * @return The ID class for the given package
    105      */
    106     private static Class<?> getIdClass(String packageName, String sourceDir)
    107             throws ClassNotFoundException {
    108         // This kind of reflection is expensive, so let's only do it
    109         // if we need to
    110         Class<?> klass = sClassMap.get(packageName);
    111         if (klass == null) {
    112             DexClassLoader classLoader = new DexClassLoader(
    113                     sourceDir, "/data/local/tmp",
    114                     null, ClassLoader.getSystemClassLoader());
    115             klass = classLoader.loadClass(packageName + ".R$id");
    116             sClassMap.put(packageName, klass);
    117         }
    118         return klass;
    119     }
    120 
    121     private static String getPositionFromNode(AccessibilityNodeInfo node) {
    122         Rect nodePosition = new Rect();
    123         node.getBoundsInScreen(nodePosition);
    124         StringBuilder positions = new StringBuilder();
    125         positions.append(nodePosition.left).append(" ").append(nodePosition.top);
    126         positions.append(" ").append(nodePosition.right-nodePosition.left).append(" ");
    127         positions.append(nodePosition.bottom-nodePosition.top);
    128         return positions.toString();
    129     }
    130 
    131 
    132     /**
    133      * Converts a resource identifier into it's generated integer ID
    134      *
    135      * @param stringId the string identifier
    136      * @return the generated integer identifier.
    137      */
    138     private static int getId(String stringId, AccessibilityEvent event)
    139             throws MonkeyViewException {
    140         try {
    141             AccessibilityNodeInfo node = event.getSource();
    142             String packageName = node.getPackageName().toString();
    143             ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0, UserId.myUserId());
    144             Class<?> klass;
    145             klass = getIdClass(packageName, appInfo.sourceDir);
    146             return klass.getField(stringId).getInt(null);
    147         } catch (RemoteException e) {
    148             throw new MonkeyViewException(REMOTE_ERROR);
    149         } catch (ClassNotFoundException e){
    150             throw new MonkeyViewException(e.getMessage());
    151         } catch (NoSuchFieldException e){
    152             throw new MonkeyViewException("No such node with given id");
    153         } catch (IllegalAccessException e){
    154             throw new MonkeyViewException("Private identifier");
    155         } catch (NullPointerException e) {
    156             // AccessibilityServiceConnection throws a NullPointerException if you hand it
    157             // an ID that doesn't exist onscreen
    158             throw new MonkeyViewException("No node with given id exists onscreen");
    159         }
    160     }
    161 
    162     private static AccessibilityNodeInfo getNodeByAccessibilityIds(
    163             String windowString, String viewString) {
    164         int windowId = Integer.parseInt(windowString);
    165         int viewId = Integer.parseInt(viewString);
    166         return sUiTestAutomationBridge.findAccessibilityNodeInfoByAccessibilityId(windowId,
    167                 viewId);
    168     }
    169 
    170     private static AccessibilityNodeInfo getNodeByViewId(String viewId, AccessibilityEvent event)
    171             throws MonkeyViewException {
    172         int id = getId(viewId, event);
    173         return sUiTestAutomationBridge.findAccessibilityNodeInfoByViewId(
    174                 UiTestAutomationBridge.ACTIVE_WINDOW_ID, UiTestAutomationBridge.ROOT_NODE_ID, id);
    175     }
    176 
    177     /**
    178      * Command to list all possible view ids for the given application.
    179      * This lists all view ids regardless if they are on screen or not.
    180      */
    181     public static class ListViewsCommand implements MonkeyCommand {
    182         //listviews
    183         public MonkeyCommandReturn translateCommand(List<String> command,
    184                                                     CommandQueue queue) {
    185             AccessibilityEvent lastEvent = sUiTestAutomationBridge.getLastAccessibilityEvent();
    186             if (lastEvent == null) {
    187                 return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
    188             }
    189             AccessibilityNodeInfo node = lastEvent.getSource();
    190             /* Occasionally the API will generate an event with no source, which is essentially the
    191              * same as it generating no event at all */
    192             if (node == null) {
    193                 return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
    194             }
    195             String packageName = node.getPackageName().toString();
    196             try{
    197                 Class<?> klass;
    198                 ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0, UserId.myUserId());
    199                 klass = getIdClass(packageName, appInfo.sourceDir);
    200                 StringBuilder fieldBuilder = new StringBuilder();
    201                 Field[] fields = klass.getFields();
    202                 for (Field field : fields) {
    203                     fieldBuilder.append(field.getName() + " ");
    204                 }
    205                 return new MonkeyCommandReturn(true, fieldBuilder.toString());
    206             } catch (RemoteException e){
    207                 return new MonkeyCommandReturn(false, REMOTE_ERROR);
    208             } catch (ClassNotFoundException e){
    209                 return new MonkeyCommandReturn(false, CLASS_NOT_FOUND);
    210             }
    211         }
    212     }
    213 
    214     /**
    215      * A command that allows for querying of views. It takes an id type, the requisite ids,
    216      * and the command for querying the view.
    217      */
    218     public static class QueryViewCommand implements MonkeyCommand {
    219         //queryview [id type] [id(s)] [command]
    220         //queryview viewid button1 gettext
    221         //queryview accessibilityids 12 5 getparent
    222         public MonkeyCommandReturn translateCommand(List<String> command,
    223                                                     CommandQueue queue) {
    224             if (command.size() > 2) {
    225                 if (!sUiTestAutomationBridge.isConnected()) {
    226                     return new MonkeyCommandReturn(false, NO_CONNECTION);
    227                 }
    228                 AccessibilityEvent lastEvent = sUiTestAutomationBridge.getLastAccessibilityEvent();
    229                 if (lastEvent == null) {
    230                     return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
    231                 }
    232                 String idType = command.get(1);
    233                 AccessibilityNodeInfo node;
    234                 String viewQuery;
    235                 List<String> args;
    236                 if ("viewid".equals(idType)) {
    237                     try {
    238                         node = getNodeByViewId(command.get(2), lastEvent);
    239                         viewQuery = command.get(3);
    240                         args = command.subList(4, command.size());
    241                     } catch (MonkeyViewException e) {
    242                         return new MonkeyCommandReturn(false, e.getMessage());
    243                     }
    244                 } else if (idType.equals("accessibilityids")) {
    245                     try {
    246                         node = getNodeByAccessibilityIds(command.get(2), command.get(3));
    247                         viewQuery = command.get(4);
    248                         args = command.subList(5, command.size());
    249                     } catch (NumberFormatException e) {
    250                         return EARG;
    251                     }
    252                 } else {
    253                     return EARG;
    254                 }
    255                 if (node == null) {
    256                     return new MonkeyCommandReturn(false, NO_NODE);
    257                 }
    258                 ViewIntrospectionCommand getter = COMMAND_MAP.get(viewQuery);
    259                 if (getter != null) {
    260                     return getter.query(node, args);
    261                 } else {
    262                     return EARG;
    263                 }
    264             }
    265             return EARG;
    266         }
    267     }
    268 
    269     /**
    270      * A command that returns the accessibility ids of the root view.
    271      */
    272     public static class GetRootViewCommand implements MonkeyCommand {
    273         // getrootview
    274         public MonkeyCommandReturn translateCommand(List<String> command,
    275                                                     CommandQueue queue) {
    276             AccessibilityEvent lastEvent = sUiTestAutomationBridge.getLastAccessibilityEvent();
    277             if (lastEvent == null) {
    278                 return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
    279             }
    280             AccessibilityNodeInfo node = lastEvent.getSource();
    281             return (new GetAccessibilityIds()).query(node, new ArrayList<String>());
    282         }
    283     }
    284 
    285     /**
    286      * A command that returns the accessibility ids of the views that contain the given text.
    287      * It takes a string of text and returns the accessibility ids of the nodes that contain the
    288      * text as a list of integers separated by spaces.
    289      */
    290     public static class GetViewsWithTextCommand implements MonkeyCommand {
    291         // getviewswithtext [text]
    292         // getviewswithtext "some text here"
    293         public MonkeyCommandReturn translateCommand(List<String> command,
    294                                                     CommandQueue queue) {
    295             if (!sUiTestAutomationBridge.isConnected()) {
    296                 return new MonkeyCommandReturn(false, NO_CONNECTION);
    297             }
    298             if (command.size() == 2) {
    299                 String text = command.get(1);
    300                 List<AccessibilityNodeInfo> nodes = sUiTestAutomationBridge
    301                     .findAccessibilityNodeInfosByText(UiTestAutomationBridge.ACTIVE_WINDOW_ID,
    302                             UiTestAutomationBridge.ROOT_NODE_ID, text);
    303                 ViewIntrospectionCommand idGetter = new GetAccessibilityIds();
    304                 List<String> emptyArgs = new ArrayList<String>();
    305                 StringBuilder ids = new StringBuilder();
    306                 for (AccessibilityNodeInfo node : nodes) {
    307                     MonkeyCommandReturn result = idGetter.query(node, emptyArgs);
    308                     if (!result.wasSuccessful()){
    309                         return result;
    310                     }
    311                     ids.append(result.getMessage()).append(" ");
    312                 }
    313                 return new MonkeyCommandReturn(true, ids.toString());
    314             }
    315             return EARG;
    316         }
    317     }
    318 
    319     /**
    320      * Command to retrieve the location of the given node.
    321      * Returns the x, y, width and height of the view, separated by spaces.
    322      */
    323     public static class GetLocation implements ViewIntrospectionCommand {
    324         //queryview [id type] [id] getlocation
    325         //queryview viewid button1 getlocation
    326         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    327                                          List<String> args) {
    328             if (args.size() == 0) {
    329                 Rect nodePosition = new Rect();
    330                 node.getBoundsInScreen(nodePosition);
    331                 StringBuilder positions = new StringBuilder();
    332                 positions.append(nodePosition.left).append(" ").append(nodePosition.top);
    333                 positions.append(" ").append(nodePosition.right-nodePosition.left).append(" ");
    334                 positions.append(nodePosition.bottom-nodePosition.top);
    335                 return new MonkeyCommandReturn(true, positions.toString());
    336             }
    337             return EARG;
    338         }
    339     }
    340 
    341 
    342     /**
    343      * Command to retrieve the text of the given node
    344      */
    345     public static class GetText implements ViewIntrospectionCommand {
    346         //queryview [id type] [id] gettext
    347         //queryview viewid button1 gettext
    348         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    349                                          List<String> args) {
    350             if (args.size() == 0) {
    351                 if (node.isPassword()){
    352                     return new MonkeyCommandReturn(false, "Node contains a password");
    353                 }
    354                 /* Occasionally we get a null from the accessibility API, rather than an empty
    355                  * string */
    356                 if (node.getText() == null) {
    357                     return new MonkeyCommandReturn(true, "");
    358                 }
    359                 return new MonkeyCommandReturn(true, node.getText().toString());
    360             }
    361             return EARG;
    362         }
    363     }
    364 
    365 
    366     /**
    367      * Command to retrieve the class name of the given node
    368      */
    369     public static class GetClass implements ViewIntrospectionCommand {
    370         //queryview [id type] [id] getclass
    371         //queryview viewid button1 getclass
    372         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    373                                          List<String> args) {
    374             if (args.size() == 0) {
    375                 return new MonkeyCommandReturn(true, node.getClassName().toString());
    376             }
    377             return EARG;
    378         }
    379     }
    380     /**
    381      * Command to retrieve the checked status of the given node
    382      */
    383     public static class GetChecked implements ViewIntrospectionCommand {
    384         //queryview [id type] [id] getchecked
    385         //queryview viewid button1 getchecked
    386         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    387                                          List<String> args) {
    388             if (args.size() == 0) {
    389                 return new MonkeyCommandReturn(true, Boolean.toString(node.isChecked()));
    390             }
    391             return EARG;
    392         }
    393     }
    394 
    395     /**
    396      * Command to retrieve whether the given node is enabled
    397      */
    398     public static class GetEnabled implements ViewIntrospectionCommand {
    399         //queryview [id type] [id] getenabled
    400         //queryview viewid button1 getenabled
    401         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    402                                          List<String> args) {
    403             if (args.size() == 0) {
    404                 return new MonkeyCommandReturn(true, Boolean.toString(node.isEnabled()));
    405             }
    406             return EARG;
    407         }
    408     }
    409 
    410     /**
    411      * Command to retrieve whether the given node is selected
    412      */
    413     public static class GetSelected implements ViewIntrospectionCommand {
    414         //queryview [id type] [id] getselected
    415         //queryview viewid button1 getselected
    416         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    417                                          List<String> args) {
    418             if (args.size() == 0) {
    419                 return new MonkeyCommandReturn(true, Boolean.toString(node.isSelected()));
    420             }
    421             return EARG;
    422         }
    423     }
    424 
    425     /**
    426      * Command to set the selected status of the given node. Takes a boolean value as its only
    427      * argument.
    428      */
    429     public static class SetSelected implements ViewIntrospectionCommand {
    430         //queryview [id type] [id] setselected [boolean]
    431         //queryview viewid button1 setselected true
    432         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    433                                          List<String> args) {
    434             if (args.size() == 1) {
    435                 boolean actionPerformed;
    436                 if (Boolean.valueOf(args.get(0))) {
    437                     actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_SELECT);
    438                 } else if (!Boolean.valueOf(args.get(0))) {
    439                     actionPerformed =
    440                             node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
    441                 } else {
    442                     return EARG;
    443                 }
    444                 return new MonkeyCommandReturn(actionPerformed);
    445             }
    446             return EARG;
    447         }
    448     }
    449 
    450     /**
    451      * Command to get whether the given node is focused.
    452      */
    453     public static class GetFocused implements ViewIntrospectionCommand {
    454         //queryview [id type] [id] getfocused
    455         //queryview viewid button1 getfocused
    456         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    457                                          List<String> args) {
    458             if (args.size() == 0) {
    459                 return new MonkeyCommandReturn(true, Boolean.toString(node.isFocused()));
    460             }
    461             return EARG;
    462         }
    463     }
    464 
    465     /**
    466      * Command to set the focus status of the given node. Takes a boolean value
    467      * as its only argument.
    468      */
    469     public static class SetFocused implements ViewIntrospectionCommand {
    470         //queryview [id type] [id] setfocused [boolean]
    471         //queryview viewid button1 setfocused false
    472         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    473                                          List<String> args) {
    474             if (args.size() == 1) {
    475                 boolean actionPerformed;
    476                 if (Boolean.valueOf(args.get(0))) {
    477                     actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
    478                 } else if (!Boolean.valueOf(args.get(0))) {
    479                     actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
    480                 } else {
    481                     return EARG;
    482                 }
    483                 return new MonkeyCommandReturn(actionPerformed);
    484             }
    485             return EARG;
    486         }
    487     }
    488 
    489     /**
    490      * Command to get the accessibility ids of the given node. Returns the accessibility ids as a
    491      * space separated pair of integers with window id coming first, followed by the accessibility
    492      * view id.
    493      */
    494     public static class GetAccessibilityIds implements ViewIntrospectionCommand {
    495         //queryview [id type] [id] getaccessibilityids
    496         //queryview viewid button1 getaccessibilityids
    497         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    498                                          List<String> args) {
    499             if (args.size() == 0) {
    500                 int viewId;
    501                 try {
    502                     Class<?> klass = node.getClass();
    503                     Field field = klass.getDeclaredField("mAccessibilityViewId");
    504                     field.setAccessible(true);
    505                     viewId = ((Integer) field.get(node)).intValue();
    506                 } catch (NoSuchFieldException e) {
    507                     return new MonkeyCommandReturn(false, NO_NODE);
    508                 } catch (IllegalAccessException e) {
    509                     return new MonkeyCommandReturn(false, "Access exception");
    510                 }
    511                 String ids = node.getWindowId() + " " + viewId;
    512                 return new MonkeyCommandReturn(true, ids);
    513             }
    514             return EARG;
    515         }
    516     }
    517 
    518     /**
    519      * Command to get the accessibility ids of the parent of the given node. Returns the
    520      * accessibility ids as a space separated pair of integers with window id coming first followed
    521      * by the accessibility view id.
    522      */
    523     public static class GetParent implements ViewIntrospectionCommand {
    524         //queryview [id type] [id] getparent
    525         //queryview viewid button1 getparent
    526         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    527                                          List<String> args) {
    528             if (args.size() == 0) {
    529                 AccessibilityNodeInfo parent = node.getParent();
    530                 if (parent == null) {
    531                   return new MonkeyCommandReturn(false, "Given node has no parent");
    532                 }
    533                 return (new GetAccessibilityIds()).query(parent, new ArrayList<String>());
    534             }
    535             return EARG;
    536         }
    537     }
    538 
    539     /**
    540      * Command to get the accessibility ids of the children of the given node. Returns the
    541      * children's ids as a space separated list of integer pairs. Each of the pairs consists of the
    542      * window id, followed by the accessibility id.
    543      */
    544     public static class GetChildren implements ViewIntrospectionCommand {
    545         //queryview [id type] [id] getchildren
    546         //queryview viewid button1 getchildren
    547         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    548                                          List<String> args) {
    549             if (args.size() == 0) {
    550                 ViewIntrospectionCommand idGetter = new GetAccessibilityIds();
    551                 List<String> emptyArgs = new ArrayList<String>();
    552                 StringBuilder ids = new StringBuilder();
    553                 int totalChildren = node.getChildCount();
    554                 for (int i = 0; i < totalChildren; i++) {
    555                     MonkeyCommandReturn result = idGetter.query(node.getChild(i), emptyArgs);
    556                     if (!result.wasSuccessful()) {
    557                         return result;
    558                     } else {
    559                         ids.append(result.getMessage()).append(" ");
    560                     }
    561                 }
    562                 return new MonkeyCommandReturn(true, ids.toString());
    563             }
    564             return EARG;
    565         }
    566     }
    567 }
    568