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          */
query(AccessibilityNodeInfo node, List<String> args)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      */
setup()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 
teardown()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      */
getIdClass(String packageName, String sourceDir)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 
getNodeByAccessibilityIds( String windowString, String viewString)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 
getNodeByViewId(String viewId)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
translateCommand(List<String> command, CommandQueue queue)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
translateCommand(List<String> command, CommandQueue queue)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
translateCommand(List<String> command, CommandQueue queue)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"
translateCommand(List<String> command, CommandQueue queue)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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
query(AccessibilityNodeInfo node, List<String> args)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