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,
    143                 false, 0);
    144     }
    145 
    146     private static AccessibilityNodeInfo getNodeByViewId(String viewId) throws MonkeyViewException {
    147         int connectionId = sUiTestAutomationBridge.getConnectionId();
    148         AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
    149         List<AccessibilityNodeInfo> infos = client.findAccessibilityNodeInfosByViewId(
    150                 connectionId, AccessibilityNodeInfo.ACTIVE_WINDOW_ID,
    151                 AccessibilityNodeInfo.ROOT_NODE_ID, viewId);
    152         return (!infos.isEmpty()) ? infos.get(0) : null;
    153     }
    154 
    155     /**
    156      * Command to list all possible view ids for the given application.
    157      * This lists all view ids regardless if they are on screen or not.
    158      */
    159     public static class ListViewsCommand implements MonkeyCommand {
    160         //listviews
    161         public MonkeyCommandReturn translateCommand(List<String> command,
    162                                                     CommandQueue queue) {
    163             AccessibilityNodeInfo node = sUiTestAutomationBridge.getRootInActiveWindow();
    164             /* Occasionally the API will generate an event with no source, which is essentially the
    165              * same as it generating no event at all */
    166             if (node == null) {
    167                 return new MonkeyCommandReturn(false, NO_ACCESSIBILITY_EVENT);
    168             }
    169             String packageName = node.getPackageName().toString();
    170             try{
    171                 Class<?> klass;
    172                 ApplicationInfo appInfo = sPm.getApplicationInfo(packageName, 0,
    173                         UserHandle.myUserId());
    174                 klass = getIdClass(packageName, appInfo.sourceDir);
    175                 StringBuilder fieldBuilder = new StringBuilder();
    176                 Field[] fields = klass.getFields();
    177                 for (Field field : fields) {
    178                     fieldBuilder.append(field.getName() + " ");
    179                 }
    180                 return new MonkeyCommandReturn(true, fieldBuilder.toString());
    181             } catch (RemoteException e){
    182                 return new MonkeyCommandReturn(false, REMOTE_ERROR);
    183             } catch (ClassNotFoundException e){
    184                 return new MonkeyCommandReturn(false, CLASS_NOT_FOUND);
    185             }
    186         }
    187     }
    188 
    189     /**
    190      * A command that allows for querying of views. It takes an id type, the requisite ids,
    191      * and the command for querying the view.
    192      */
    193     public static class QueryViewCommand implements MonkeyCommand {
    194         //queryview [id type] [id(s)] [command]
    195         //queryview viewid button1 gettext
    196         //queryview accessibilityids 12 5 getparent
    197         public MonkeyCommandReturn translateCommand(List<String> command,
    198                                                     CommandQueue queue) {
    199             if (command.size() > 2) {
    200                 String idType = command.get(1);
    201                 AccessibilityNodeInfo node;
    202                 String viewQuery;
    203                 List<String> args;
    204                 if ("viewid".equals(idType)) {
    205                     try {
    206                         node = getNodeByViewId(command.get(2));
    207                         viewQuery = command.get(3);
    208                         args = command.subList(4, command.size());
    209                     } catch (MonkeyViewException e) {
    210                         return new MonkeyCommandReturn(false, e.getMessage());
    211                     }
    212                 } else if (idType.equals("accessibilityids")) {
    213                     try {
    214                         node = getNodeByAccessibilityIds(command.get(2), command.get(3));
    215                         viewQuery = command.get(4);
    216                         args = command.subList(5, command.size());
    217                     } catch (NumberFormatException e) {
    218                         return EARG;
    219                     }
    220                 } else {
    221                     return EARG;
    222                 }
    223                 if (node == null) {
    224                     return new MonkeyCommandReturn(false, NO_NODE);
    225                 }
    226                 ViewIntrospectionCommand getter = COMMAND_MAP.get(viewQuery);
    227                 if (getter != null) {
    228                     return getter.query(node, args);
    229                 } else {
    230                     return EARG;
    231                 }
    232             }
    233             return EARG;
    234         }
    235     }
    236 
    237     /**
    238      * A command that returns the accessibility ids of the root view.
    239      */
    240     public static class GetRootViewCommand implements MonkeyCommand {
    241         // getrootview
    242         public MonkeyCommandReturn translateCommand(List<String> command,
    243                                                     CommandQueue queue) {
    244             AccessibilityNodeInfo node = sUiTestAutomationBridge.getRootInActiveWindow();
    245             return (new GetAccessibilityIds()).query(node, new ArrayList<String>());
    246         }
    247     }
    248 
    249     /**
    250      * A command that returns the accessibility ids of the views that contain the given text.
    251      * It takes a string of text and returns the accessibility ids of the nodes that contain the
    252      * text as a list of integers separated by spaces.
    253      */
    254     public static class GetViewsWithTextCommand implements MonkeyCommand {
    255         // getviewswithtext [text]
    256         // getviewswithtext "some text here"
    257         public MonkeyCommandReturn translateCommand(List<String> command,
    258                                                     CommandQueue queue) {
    259             if (command.size() == 2) {
    260                 String text = command.get(1);
    261                 int connectionId = sUiTestAutomationBridge.getConnectionId();
    262                 List<AccessibilityNodeInfo> nodes = AccessibilityInteractionClient.getInstance()
    263                     .findAccessibilityNodeInfosByText(connectionId,
    264                             AccessibilityNodeInfo.ACTIVE_WINDOW_ID,
    265                             AccessibilityNodeInfo.ROOT_NODE_ID, text);
    266                 ViewIntrospectionCommand idGetter = new GetAccessibilityIds();
    267                 List<String> emptyArgs = new ArrayList<String>();
    268                 StringBuilder ids = new StringBuilder();
    269                 for (AccessibilityNodeInfo node : nodes) {
    270                     MonkeyCommandReturn result = idGetter.query(node, emptyArgs);
    271                     if (!result.wasSuccessful()){
    272                         return result;
    273                     }
    274                     ids.append(result.getMessage()).append(" ");
    275                 }
    276                 return new MonkeyCommandReturn(true, ids.toString());
    277             }
    278             return EARG;
    279         }
    280     }
    281 
    282     /**
    283      * Command to retrieve the location of the given node.
    284      * Returns the x, y, width and height of the view, separated by spaces.
    285      */
    286     public static class GetLocation implements ViewIntrospectionCommand {
    287         //queryview [id type] [id] getlocation
    288         //queryview viewid button1 getlocation
    289         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    290                                          List<String> args) {
    291             if (args.size() == 0) {
    292                 Rect nodePosition = new Rect();
    293                 node.getBoundsInScreen(nodePosition);
    294                 StringBuilder positions = new StringBuilder();
    295                 positions.append(nodePosition.left).append(" ").append(nodePosition.top);
    296                 positions.append(" ").append(nodePosition.right-nodePosition.left).append(" ");
    297                 positions.append(nodePosition.bottom-nodePosition.top);
    298                 return new MonkeyCommandReturn(true, positions.toString());
    299             }
    300             return EARG;
    301         }
    302     }
    303 
    304 
    305     /**
    306      * Command to retrieve the text of the given node
    307      */
    308     public static class GetText implements ViewIntrospectionCommand {
    309         //queryview [id type] [id] gettext
    310         //queryview viewid button1 gettext
    311         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    312                                          List<String> args) {
    313             if (args.size() == 0) {
    314                 if (node.isPassword()){
    315                     return new MonkeyCommandReturn(false, "Node contains a password");
    316                 }
    317                 /* Occasionally we get a null from the accessibility API, rather than an empty
    318                  * string */
    319                 if (node.getText() == null) {
    320                     return new MonkeyCommandReturn(true, "");
    321                 }
    322                 return new MonkeyCommandReturn(true, node.getText().toString());
    323             }
    324             return EARG;
    325         }
    326     }
    327 
    328 
    329     /**
    330      * Command to retrieve the class name of the given node
    331      */
    332     public static class GetClass implements ViewIntrospectionCommand {
    333         //queryview [id type] [id] getclass
    334         //queryview viewid button1 getclass
    335         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    336                                          List<String> args) {
    337             if (args.size() == 0) {
    338                 return new MonkeyCommandReturn(true, node.getClassName().toString());
    339             }
    340             return EARG;
    341         }
    342     }
    343     /**
    344      * Command to retrieve the checked status of the given node
    345      */
    346     public static class GetChecked implements ViewIntrospectionCommand {
    347         //queryview [id type] [id] getchecked
    348         //queryview viewid button1 getchecked
    349         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    350                                          List<String> args) {
    351             if (args.size() == 0) {
    352                 return new MonkeyCommandReturn(true, Boolean.toString(node.isChecked()));
    353             }
    354             return EARG;
    355         }
    356     }
    357 
    358     /**
    359      * Command to retrieve whether the given node is enabled
    360      */
    361     public static class GetEnabled implements ViewIntrospectionCommand {
    362         //queryview [id type] [id] getenabled
    363         //queryview viewid button1 getenabled
    364         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    365                                          List<String> args) {
    366             if (args.size() == 0) {
    367                 return new MonkeyCommandReturn(true, Boolean.toString(node.isEnabled()));
    368             }
    369             return EARG;
    370         }
    371     }
    372 
    373     /**
    374      * Command to retrieve whether the given node is selected
    375      */
    376     public static class GetSelected implements ViewIntrospectionCommand {
    377         //queryview [id type] [id] getselected
    378         //queryview viewid button1 getselected
    379         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    380                                          List<String> args) {
    381             if (args.size() == 0) {
    382                 return new MonkeyCommandReturn(true, Boolean.toString(node.isSelected()));
    383             }
    384             return EARG;
    385         }
    386     }
    387 
    388     /**
    389      * Command to set the selected status of the given node. Takes a boolean value as its only
    390      * argument.
    391      */
    392     public static class SetSelected implements ViewIntrospectionCommand {
    393         //queryview [id type] [id] setselected [boolean]
    394         //queryview viewid button1 setselected true
    395         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    396                                          List<String> args) {
    397             if (args.size() == 1) {
    398                 boolean actionPerformed;
    399                 if (Boolean.valueOf(args.get(0))) {
    400                     actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_SELECT);
    401                 } else if (!Boolean.valueOf(args.get(0))) {
    402                     actionPerformed =
    403                             node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION);
    404                 } else {
    405                     return EARG;
    406                 }
    407                 return new MonkeyCommandReturn(actionPerformed);
    408             }
    409             return EARG;
    410         }
    411     }
    412 
    413     /**
    414      * Command to get whether the given node is focused.
    415      */
    416     public static class GetFocused implements ViewIntrospectionCommand {
    417         //queryview [id type] [id] getfocused
    418         //queryview viewid button1 getfocused
    419         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    420                                          List<String> args) {
    421             if (args.size() == 0) {
    422                 return new MonkeyCommandReturn(true, Boolean.toString(node.isFocused()));
    423             }
    424             return EARG;
    425         }
    426     }
    427 
    428     /**
    429      * Command to set the focus status of the given node. Takes a boolean value
    430      * as its only argument.
    431      */
    432     public static class SetFocused implements ViewIntrospectionCommand {
    433         //queryview [id type] [id] setfocused [boolean]
    434         //queryview viewid button1 setfocused false
    435         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    436                                          List<String> args) {
    437             if (args.size() == 1) {
    438                 boolean actionPerformed;
    439                 if (Boolean.valueOf(args.get(0))) {
    440                     actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
    441                 } else if (!Boolean.valueOf(args.get(0))) {
    442                     actionPerformed = node.performAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS);
    443                 } else {
    444                     return EARG;
    445                 }
    446                 return new MonkeyCommandReturn(actionPerformed);
    447             }
    448             return EARG;
    449         }
    450     }
    451 
    452     /**
    453      * Command to get the accessibility ids of the given node. Returns the accessibility ids as a
    454      * space separated pair of integers with window id coming first, followed by the accessibility
    455      * view id.
    456      */
    457     public static class GetAccessibilityIds implements ViewIntrospectionCommand {
    458         //queryview [id type] [id] getaccessibilityids
    459         //queryview viewid button1 getaccessibilityids
    460         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    461                                          List<String> args) {
    462             if (args.size() == 0) {
    463                 int viewId;
    464                 try {
    465                     Class<?> klass = node.getClass();
    466                     Field field = klass.getDeclaredField("mAccessibilityViewId");
    467                     field.setAccessible(true);
    468                     viewId = ((Integer) field.get(node)).intValue();
    469                 } catch (NoSuchFieldException e) {
    470                     return new MonkeyCommandReturn(false, NO_NODE);
    471                 } catch (IllegalAccessException e) {
    472                     return new MonkeyCommandReturn(false, "Access exception");
    473                 }
    474                 String ids = node.getWindowId() + " " + viewId;
    475                 return new MonkeyCommandReturn(true, ids);
    476             }
    477             return EARG;
    478         }
    479     }
    480 
    481     /**
    482      * Command to get the accessibility ids of the parent of the given node. Returns the
    483      * accessibility ids as a space separated pair of integers with window id coming first followed
    484      * by the accessibility view id.
    485      */
    486     public static class GetParent implements ViewIntrospectionCommand {
    487         //queryview [id type] [id] getparent
    488         //queryview viewid button1 getparent
    489         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    490                                          List<String> args) {
    491             if (args.size() == 0) {
    492                 AccessibilityNodeInfo parent = node.getParent();
    493                 if (parent == null) {
    494                   return new MonkeyCommandReturn(false, "Given node has no parent");
    495                 }
    496                 return (new GetAccessibilityIds()).query(parent, new ArrayList<String>());
    497             }
    498             return EARG;
    499         }
    500     }
    501 
    502     /**
    503      * Command to get the accessibility ids of the children of the given node. Returns the
    504      * children's ids as a space separated list of integer pairs. Each of the pairs consists of the
    505      * window id, followed by the accessibility id.
    506      */
    507     public static class GetChildren implements ViewIntrospectionCommand {
    508         //queryview [id type] [id] getchildren
    509         //queryview viewid button1 getchildren
    510         public MonkeyCommandReturn query(AccessibilityNodeInfo node,
    511                                          List<String> args) {
    512             if (args.size() == 0) {
    513                 ViewIntrospectionCommand idGetter = new GetAccessibilityIds();
    514                 List<String> emptyArgs = new ArrayList<String>();
    515                 StringBuilder ids = new StringBuilder();
    516                 int totalChildren = node.getChildCount();
    517                 for (int i = 0; i < totalChildren; i++) {
    518                     MonkeyCommandReturn result = idGetter.query(node.getChild(i), emptyArgs);
    519                     if (!result.wasSuccessful()) {
    520                         return result;
    521                     } else {
    522                         ids.append(result.getMessage()).append(" ");
    523                     }
    524                 }
    525                 return new MonkeyCommandReturn(true, ids.toString());
    526             }
    527             return EARG;
    528         }
    529     }
    530 }
    531