QueryController.java revision e54d649fb83a0a44516e5c25a9ac1992c8950e59
1/* 2 * Copyright (C) 2012 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 */ 16package com.android.uiautomator.core; 17 18import android.os.SystemClock; 19import android.util.Log; 20import android.view.accessibility.AccessibilityEvent; 21import android.view.accessibility.AccessibilityNodeInfo; 22 23import com.android.uiautomator.core.UiAutomatorBridge.AccessibilityEventListener; 24 25/** 26 * The QuertController main purpose is to translate a {@link By} selectors to 27 * {@link AccessibilityNodeInfo}. This is all this controller does. It is typically 28 * created in conjunction with a {@link InteractionController} by {@link UiAutomationContext} 29 * which owns both. {@link UiAutomationContext} is used by {@link UiBase} classes. 30 */ 31class QueryController { 32 33 private static final String LOG_TAG = QueryController.class.getSimpleName(); 34 35 private static final boolean DEBUG = false; 36 37 private final UiAutomatorBridge mUiAutomatorBridge; 38 39 private final Object mLock = new Object(); 40 41 private String mLastActivityName = null; 42 private String mLastPackageName = null; 43 44 // During a pattern selector search, the recursive pattern search 45 // methods will track their counts and indexes here. 46 private int mPatternCounter = 0; 47 private int mPatternIndexer = 0; 48 49 // These help show each selector's search context as it relates to the previous sub selector 50 // matched. When a compound selector fails, it is hard to tell which part of it is failing. 51 // Seeing how a selector is being parsed and which sub selector failed within a long list 52 // of compound selectors is very helpful. 53 private int mLogIndent = 0; 54 private int mLogParentIndent = 0; 55 56 private String mLastTraversedText = ""; 57 58 public QueryController(UiAutomatorBridge bridge) { 59 mUiAutomatorBridge = bridge; 60 bridge.addAccessibilityEventListener(new AccessibilityEventListener() { 61 @Override 62 public void onAccessibilityEvent(AccessibilityEvent event) { 63 synchronized (mLock) { 64 mLastPackageName = event.getPackageName().toString(); 65 // don't trust event.getText(), check for nulls 66 if (event.getText() != null && event.getText().size() > 0) { 67 if(event.getText().get(0) != null) 68 mLastActivityName = event.getText().get(0).toString(); 69 } 70 71 switch(event.getEventType()) { 72 case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: 73 // don't trust event.getText(), check for nulls 74 if (event.getText() != null && event.getText().size() > 0) 75 if(event.getText().get(0) != null) 76 mLastTraversedText = event.getText().get(0).toString(); 77 if(DEBUG) 78 Log.i(LOG_TAG, "Last text selection reported: " + mLastTraversedText); 79 break; 80 } 81 mLock.notifyAll(); 82 } 83 } 84 }); 85 } 86 87 /** 88 * Returns the last text selection reported by accessibility 89 * event TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY. One way to cause 90 * this event is using a DPad arrows to focus on UI elements. 91 * @return 92 */ 93 public String getLastTraversedText() { 94 mUiAutomatorBridge.waitForIdle(); 95 synchronized (mLock) { 96 if (mLastTraversedText.length() > 0) { 97 return mLastTraversedText; 98 } 99 } 100 return null; 101 } 102 103 /** 104 * Clears the last text selection value saved from the TYPE_VIEW_TEXT_SELECTION_CHANGED 105 * event 106 */ 107 public void clearLastTraversedText() { 108 mUiAutomatorBridge.waitForIdle(); 109 synchronized (mLock) { 110 mLastTraversedText = ""; 111 } 112 } 113 114 private void initializeNewSearch() { 115 mPatternCounter = 0; 116 mPatternIndexer = 0; 117 mLogIndent = 0; 118 mLogParentIndent = 0;; 119 } 120 121 /** 122 * Counts the instances of the selector group. The selector must be in the following 123 * format: [container_selector, PATTERN=[INSTANCE=x, PATTERN=[the_pattern]] 124 * where the container_selector is used to find the containment region to search for patterns 125 * and the INSTANCE=x is the instance of the_pattern to return. 126 * @param selector 127 * @return number of pattern matches. Returns 0 for all other cases. 128 */ 129 public int getPatternCount(By selector) { 130 findAccessibilityNodeInfo(selector, true /*counting*/); 131 return mPatternCounter; 132 } 133 134 /** 135 * Main search method for translating By selectors to AccessibilityInfoNodes 136 * @param selector 137 * @return 138 */ 139 public AccessibilityNodeInfo findAccessibilityNodeInfo(By selector) { 140 return findAccessibilityNodeInfo(selector, false); 141 } 142 143 protected AccessibilityNodeInfo findAccessibilityNodeInfo(By selector, boolean isCounting) { 144 mUiAutomatorBridge.waitForIdle(); 145 initializeNewSearch(); 146 147 if(DEBUG) 148 Log.i(LOG_TAG, "Searching: " + selector); 149 150 synchronized (mLock) { 151 AccessibilityNodeInfo rootNode = getRootNode(); 152 if (rootNode == null) { 153 Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search"); 154 return null; 155 } 156 157 // Copy so that we don't modify the original's sub selectors 158 By bySelector = By.selector(selector); 159 return translateCompoundSelector(bySelector, rootNode, isCounting); 160 } 161 } 162 163 /** 164 * Gets the root node from accessibility and if it fails to get one it will 165 * retry every 250ms for up to 1000ms. 166 * @return null if no root node is obtained 167 */ 168 protected AccessibilityNodeInfo getRootNode() { 169 final int maxRetry = 4; 170 final long waitInterval = 250; 171 AccessibilityNodeInfo rootNode = null; 172 for(int x = 0; x < maxRetry; x++) { 173 rootNode = mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow(); 174 if (rootNode != null) { 175 return rootNode; 176 } 177 if(x < maxRetry - 1) { 178 Log.e(LOG_TAG, "Got null root node from accessibility - Retrying..."); 179 SystemClock.sleep(waitInterval); 180 } 181 } 182 return rootNode; 183 } 184 185 /** 186 * A compoundSelector encapsulate both Regular and Pattern selectors. The formats follows: 187 * <p/> 188 * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]] 189 * <br/> 190 * pattern_selector = ...CONTAINER=By[..] PATTERN=By[instance=x PATTERN=[regular_selector] 191 * <br/> 192 * compound_selector = [regular_selector [pattern_selector]] 193 * <p/> 194 * regular_selectors are the most common form of selectors and the search for them 195 * is straightforward. On the other hand pattern_selectors requires search to be 196 * performed as in regular_selector but where regular_selector search returns immediately 197 * upon a successful match, the search for pattern_selector continues until the 198 * requested matched _instance_ of that pattern is matched. 199 * <p/> 200 * Counting UI objects requires using pattern_selectors. The counting search is the same 201 * as a pattern_search however we're not looking to match an instance of the pattern but 202 * rather continuously walking the accessibility node hierarchy while counting matched 203 * patterns, until the end of the tree. 204 * <p/> 205 * If both present, order of parsing begins with CONTAINER followed by PATTERN then the 206 * top most selector is processed as regular_selector within the context of the previous 207 * CONTAINER and its PATTERN information. If neither is present then the top selector is 208 * directly treated as regular_selector. So the presence of a CONTAINER and PATTERN within 209 * a selector simply dictates that the selector matching will be constraint to the sub tree 210 * node where the CONTAINER and its child PATTERN have identified. 211 * @param bySelector 212 * @param fromNode 213 * @param isCounting 214 * @return 215 */ 216 private AccessibilityNodeInfo translateCompoundSelector(By bySelector, 217 AccessibilityNodeInfo fromNode, boolean isCounting) { 218 219 // Start translating compound selectors by translating the regular_selector first 220 // The regular_selector is then used as a container for any optional pattern_selectors 221 // that may or may not be specified. 222 if(bySelector.hasContainerSelector()) 223 // nested pattern selectors 224 if(bySelector.getContainerSelector().hasContainerSelector()) { 225 fromNode = translateCompoundSelector( 226 bySelector.getContainerSelector(), fromNode, false); 227 initializeNewSearch(); 228 } else 229 fromNode = translateReqularSelector(bySelector.getContainerSelector(), fromNode); 230 else 231 fromNode = translateReqularSelector(bySelector, fromNode); 232 233 if(fromNode == null) { 234 if(DEBUG) 235 Log.i(LOG_TAG, "Container selector not found: " + bySelector.dumpToString(false)); 236 return null; 237 } 238 239 if(bySelector.hasPatternSelector()) { 240 fromNode = translatePatternSelector(bySelector.getPatternSelector(), 241 fromNode, isCounting); 242 243 if (isCounting) { 244 Log.i(LOG_TAG, String.format( 245 "Counted %d instances of: %s", mPatternCounter, bySelector)); 246 return null; 247 } else { 248 if(fromNode == null) { 249 if(DEBUG) 250 Log.i(LOG_TAG, "Pattern selector not found: " + 251 bySelector.dumpToString(false)); 252 return null; 253 } 254 } 255 } 256 257 // translate any additions to the selector that may have been added by tests 258 // with getChild(By selector) after a container and pattern selectors 259 if(bySelector.hasContainerSelector() || bySelector.hasPatternSelector()) { 260 if(bySelector.hasChildSelector() || bySelector.hasParentSelector()) 261 fromNode = translateReqularSelector(bySelector, fromNode); 262 } 263 264 if(fromNode == null) { 265 if(DEBUG) 266 Log.i(LOG_TAG, "Object Not Found for selector " + bySelector); 267 return null; 268 } 269 Log.i(LOG_TAG, String.format("Matched selector: %s <<==>> [%s]", bySelector, fromNode)); 270 return fromNode; 271 } 272 273 /** 274 * Used by the {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)} 275 * to translate the regular_selector portion. It has the following format: 276 * <p/> 277 * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]<br/> 278 * <p/> 279 * regular_selectors are the most common form of selectors and the search for them 280 * is straightforward. This method will only look for CHILD or PARENT sub selectors. 281 * <p/> 282 * @param selector 283 * @param fromNode 284 * @param index 285 * @return AccessibilityNodeInfo if found else null 286 */ 287 private AccessibilityNodeInfo translateReqularSelector(By selector, 288 AccessibilityNodeInfo fromNode) { 289 290 return findNodeRegularRecursive(selector, fromNode, 0); 291 } 292 293 private AccessibilityNodeInfo findNodeRegularRecursive(By subSelector, 294 AccessibilityNodeInfo fromNode, int index) { 295 296 if (subSelector.isMatchFor(fromNode, index)) { 297 if (DEBUG) { 298 Log.d(LOG_TAG, formatLog(String.format("%s", 299 subSelector.dumpToString(false)))); 300 } 301 if(subSelector.isLeaf()) { 302 return fromNode; 303 } 304 if(subSelector.hasChildSelector()) { 305 mLogIndent++; // next selector 306 subSelector = subSelector.getChildSelector(); 307 if(subSelector == null) { 308 Log.e(LOG_TAG, "Error: A child selector without content"); 309 return null; // there is an implementation fault 310 } 311 } else if(subSelector.hasParentSelector()) { 312 mLogIndent++; // next selector 313 subSelector = subSelector.getParentSelector(); 314 if(subSelector == null) { 315 Log.e(LOG_TAG, "Error: A parent selector without content"); 316 return null; // there is an implementation fault 317 } 318 // the selector requested we start at this level from 319 // the parent node from the one we just matched 320 fromNode = fromNode.getParent(); 321 if(fromNode == null) 322 return null; 323 } 324 } 325 326 int childCount = fromNode.getChildCount(); 327 boolean hasNullChild = false; 328 for (int i = 0; i < childCount; i++) { 329 AccessibilityNodeInfo childNode = fromNode.getChild(i); 330 if (childNode == null) { 331 Log.w(LOG_TAG, String.format( 332 "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount)); 333 if (!hasNullChild) { 334 Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString())); 335 } 336 hasNullChild = true; 337 continue; 338 } 339 if (!childNode.isVisibleToUser()) { 340 // TODO: need to remove this or move it under if (DEBUG) 341 Log.d(LOG_TAG, String.format("Skipping invisible child: %s", childNode.toString())); 342 continue; 343 } 344 AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i); 345 if (retNode != null) { 346 return retNode; 347 } 348 } 349 return null; 350 } 351 352 /** 353 * Used by the {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)} 354 * to translate the pattern_selector portion. It has the following format: 355 * <p/> 356 * pattern_selector = ... PATTERN=By[instance=x PATTERN=[regular_selector]]<br/> 357 * <p/> 358 * pattern_selectors requires search to be performed as regular_selector but where 359 * regular_selector search returns immediately upon a successful match, the search for 360 * pattern_selector continues until the requested matched instance of that pattern is 361 * encountered. 362 * <p/> 363 * Counting UI objects requires using pattern_selectors. The counting search is the same 364 * as a pattern_search however we're not looking to match an instance of the pattern but 365 * rather continuously walking the accessibility node hierarchy while counting patterns 366 * until the end of the tree. 367 * @param subSelector 368 * @param fromNode 369 * @param originalPattern 370 * @return null of node is not found or if counting mode is true. 371 * See {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)} 372 */ 373 private AccessibilityNodeInfo translatePatternSelector(By subSelector, 374 AccessibilityNodeInfo fromNode, boolean isCounting) { 375 376 if(subSelector.hasPatternSelector()) { 377 // Since pattern_selectors are also the type of selectors used when counting, 378 // we check if this is a counting run or an indexing run 379 if(isCounting) 380 //since we're counting, we reset the indexer so to terminates the search when 381 // the end of tree is reached. The count will be in mPatternCount 382 mPatternIndexer = -1; 383 else 384 // terminates the search once we match the pattern's instance 385 mPatternIndexer = subSelector.getInstance(); 386 387 // A pattern is wrapped in a PATTERN[instance=x PATTERN[the_pattern]] 388 subSelector = subSelector.getPatternSelector(); 389 if(subSelector == null) { 390 Log.e(LOG_TAG, "Pattern portion of the selector is null or not defined"); 391 return null; // there is an implementation fault 392 } 393 // save the current indent level as parent indent before pattern searches 394 // begin under the current tree position. 395 mLogParentIndent = ++mLogIndent; 396 return findNodePatternRecursive(subSelector, fromNode, 0, subSelector); 397 } 398 399 Log.e(LOG_TAG, "Selector must have a pattern selector defined"); // implementation fault? 400 return null; 401 } 402 403 private AccessibilityNodeInfo findNodePatternRecursive( 404 By subSelector, AccessibilityNodeInfo fromNode, int index, By originalPattern) { 405 406 if (subSelector.isMatchFor(fromNode, index)) { 407 if(subSelector.isLeaf()) { 408 if(mPatternIndexer == 0) { 409 if (DEBUG) 410 Log.d(LOG_TAG, formatLog( 411 String.format("%s", subSelector.dumpToString(false)))); 412 return fromNode; 413 } else { 414 if(DEBUG) 415 Log.d(LOG_TAG, formatLog( 416 String.format("%s", subSelector.dumpToString(false)))); 417 mPatternCounter++; //count the pattern matched 418 mPatternIndexer--; //decrement until zero for the instance requested 419 420 // At a leaf selector within a group and still not instance matched 421 // then reset the selector to continue search from current position 422 // in the accessibility tree for the next pattern match up until the 423 // pattern index hits 0. 424 subSelector = originalPattern; 425 // starting over with next pattern search so reset to parent level 426 mLogIndent = mLogParentIndent; 427 } 428 } else { 429 if(DEBUG) 430 Log.d(LOG_TAG, formatLog( 431 String.format("%s", subSelector.dumpToString(false)))); 432 433 if(subSelector.hasChildSelector()) { 434 mLogIndent++; // next selector 435 subSelector = subSelector.getChildSelector(); 436 if(subSelector == null) { 437 Log.e(LOG_TAG, "Error: A child selector without content"); 438 return null; 439 } 440 } else if(subSelector.hasParentSelector()) { 441 mLogIndent++; // next selector 442 subSelector = subSelector.getParentSelector(); 443 if(subSelector == null) { 444 Log.e(LOG_TAG, "Error: A parent selector without content"); 445 return null; 446 } 447 fromNode = fromNode.getParent(); 448 if(fromNode == null) 449 return null; 450 } 451 } 452 } 453 454 int childCount = fromNode.getChildCount(); 455 boolean hasNullChild = false; 456 for (int i = 0; i < childCount; i++) { 457 AccessibilityNodeInfo childNode = fromNode.getChild(i); 458 if (childNode == null) { 459 Log.w(LOG_TAG, String.format( 460 "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount)); 461 if (!hasNullChild) { 462 Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString())); 463 } 464 hasNullChild = true; 465 continue; 466 } 467 if (!childNode.isVisibleToUser()) { 468 // TODO: need to remove this or move it under if (DEBUG) 469 Log.d(LOG_TAG, String.format("Skipping invisible child: %s", childNode.toString())); 470 continue; 471 } 472 AccessibilityNodeInfo retNode = findNodePatternRecursive( 473 subSelector, childNode, i, originalPattern); 474 if (retNode != null) { 475 return retNode; 476 } 477 } 478 return null; 479 } 480 481 public AccessibilityNodeInfo getAccessibilityRootNode() { 482 return mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow(); 483 } 484 485 /** 486 * Last activity to report accessibility events 487 * @return String name of activity 488 */ 489 public String getCurrentActivityName() { 490 mUiAutomatorBridge.waitForIdle(); 491 synchronized (mLock) { 492 return mLastActivityName; 493 } 494 } 495 496 /** 497 * Last package to report accessibility events 498 * @return String name of package 499 */ 500 public String getCurrentPackageName() { 501 mUiAutomatorBridge.waitForIdle(); 502 synchronized (mLock) { 503 return mLastPackageName; 504 } 505 } 506 507 private String formatLog(String str) { 508 StringBuilder l = new StringBuilder(); 509 for(int space = 0; space < mLogIndent; space++) 510 l.append(". . "); 511 if(mLogIndent > 0) 512 l.append(String.format(". . [%d]: %s", mPatternCounter, str)); 513 else 514 l.append(String.format(". . [%d]: %s", mPatternCounter, str)); 515 return l.toString(); 516 } 517} 518