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