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