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