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.graphics.Rect; 19import android.util.Log; 20import android.view.accessibility.AccessibilityNodeInfo; 21 22/** 23 * UiScrollable is a {@link UiCollection} and provides support for searching for items in a 24 * scrollable UI elements. Used with horizontally or vertically scrollable UI. 25 * @since API Level 16 26 */ 27public class UiScrollable extends UiCollection { 28 private static final String LOG_TAG = UiScrollable.class.getSimpleName(); 29 30 // More steps slows the swipe and prevents contents from being flung too far 31 private static final int SCROLL_STEPS = 55; 32 33 private static final int FLING_STEPS = 5; 34 35 // Restrict a swipe's starting and ending points inside a 10% margin of the target 36 private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1; 37 38 // Limits the number of swipes/scrolls performed during a search 39 private static int mMaxSearchSwipes = 30; 40 41 // Used in ScrollForward() and ScrollBackward() to determine swipe direction 42 private boolean mIsVerticalList = true; 43 44 private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT; 45 46 /** 47 * UiScrollable is a {@link UiCollection} and as such requires a {@link UiSelector} to 48 * identify the container UI element of the scrollable collection. Further operations on 49 * the items in the container will require specifying UiSelector as an item selector. 50 * 51 * @param container a {@link UiSelector} selector 52 * @since API Level 16 53 */ 54 public UiScrollable(UiSelector container) { 55 // wrap the container selector with container so that QueryController can handle 56 // this type of enumeration search accordingly 57 super(container); 58 } 59 60 /** 61 * Set the direction of swipes when performing scroll search 62 * @return reference to itself 63 * @since API Level 16 64 */ 65 public UiScrollable setAsVerticalList() { 66 Tracer.trace(); 67 mIsVerticalList = true; 68 return this; 69 } 70 71 /** 72 * Set the direction of swipes when performing scroll search 73 * @return reference to itself 74 * @since API Level 16 75 */ 76 public UiScrollable setAsHorizontalList() { 77 Tracer.trace(); 78 mIsVerticalList = false; 79 return this; 80 } 81 82 /** 83 * Used privately when performing swipe searches to decide if an element has become 84 * visible or not. 85 * 86 * @param selector 87 * @return true if found else false 88 * @since API Level 16 89 */ 90 protected boolean exists(UiSelector selector) { 91 if(getQueryController().findAccessibilityNodeInfo(selector) != null) { 92 return true; 93 } 94 return false; 95 } 96 97 /** 98 * Searches for child UI element within the constraints of this UiScrollable {@link UiSelector} 99 * container. It looks for any child matching the <code>childPattern</code> argument within its 100 * hierarchy with a matching content-description text. The returned UiObject will represent the 101 * UI element matching the <code>childPattern</code> and not the sub element that matched the 102 * content description.</p> 103 * By default this operation will perform scroll search while attempting to find the UI element 104 * See {@link #getChildByDescription(UiSelector, String, boolean)} 105 * 106 * @param childPattern {@link UiSelector} selector of the child pattern to match and return 107 * @param text String of the identifying child contents of of the <code>childPattern</code> 108 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 109 * @throws UiObjectNotFoundException 110 * @since API Level 16 111 */ 112 @Override 113 public UiObject getChildByDescription(UiSelector childPattern, String text) 114 throws UiObjectNotFoundException { 115 Tracer.trace(childPattern, text); 116 return getChildByDescription(childPattern, text, true); 117 } 118 119 /** 120 * See {@link #getChildByDescription(UiSelector, String)} 121 * 122 * @param childPattern {@link UiSelector} selector of the child pattern to match and return 123 * @param text String may be a partial match for the content-description of a child element. 124 * @param allowScrollSearch set to true if scrolling is allowed 125 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 126 * @throws UiObjectNotFoundException 127 * @since API Level 16 128 */ 129 public UiObject getChildByDescription(UiSelector childPattern, String text, 130 boolean allowScrollSearch) throws UiObjectNotFoundException { 131 Tracer.trace(childPattern, text, allowScrollSearch); 132 if (text != null) { 133 if (allowScrollSearch) { 134 scrollIntoView(new UiSelector().descriptionContains(text)); 135 } 136 return super.getChildByDescription(childPattern, text); 137 } 138 throw new UiObjectNotFoundException("for description= \"" + text + "\""); 139 } 140 141 /** 142 * Searches for child UI element within the constraints of this UiScrollable {@link UiSelector} 143 * selector. It looks for any child matching the <code>childPattern</code> argument and 144 * return the <code>instance</code> specified. The operation is performed only on the visible 145 * items and no scrolling is performed in this case. 146 * 147 * @param childPattern {@link UiSelector} selector of the child pattern to match and return 148 * @param instance int the desired matched instance of this <code>childPattern</code> 149 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 150 * @since API Level 16 151 */ 152 @Override 153 public UiObject getChildByInstance(UiSelector childPattern, int instance) 154 throws UiObjectNotFoundException { 155 Tracer.trace(childPattern, instance); 156 UiSelector patternSelector = UiSelector.patternBuilder(getSelector(), 157 UiSelector.patternBuilder(childPattern).instance(instance)); 158 return new UiObject(patternSelector); 159 } 160 161 /** 162 * Searches for child UI element within the constraints of this UiScrollable {@link UiSelector} 163 * container. It looks for any child matching the <code>childPattern</code> argument that has 164 * a sub UI element anywhere within its sub hierarchy that has text attribute 165 * <code>text</code>. The returned UiObject will point at the <code>childPattern</code> 166 * instance that matched the search and not at the text matched sub element</p> 167 * By default this operation will perform scroll search while attempting to find the UI 168 * element. 169 * See {@link #getChildByText(UiSelector, String, boolean)} 170 * 171 * @param childPattern {@link UiSelector} selector of the child pattern to match and return 172 * @param text String of the identifying child contents of of the <code>childPattern</code> 173 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 174 * @throws UiObjectNotFoundException 175 * @since API Level 16 176 */ 177 @Override 178 public UiObject getChildByText(UiSelector childPattern, String text) 179 throws UiObjectNotFoundException { 180 Tracer.trace(childPattern, text); 181 return getChildByText(childPattern, text, true); 182 } 183 184 /** 185 * See {@link #getChildByText(UiSelector, String)} 186 * 187 * @param childPattern {@link UiSelector} selector of the child pattern to match and return 188 * @param text String of the identifying child contents of of the <code>childPattern</code> 189 * @param allowScrollSearch set to true if scrolling is allowed 190 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 191 * @throws UiObjectNotFoundException 192 * @since API Level 16 193 */ 194 public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch) 195 throws UiObjectNotFoundException { 196 Tracer.trace(childPattern, text, allowScrollSearch); 197 if (text != null) { 198 if (allowScrollSearch) { 199 scrollIntoView(new UiSelector().text(text)); 200 } 201 return super.getChildByText(childPattern, text); 202 } 203 throw new UiObjectNotFoundException("for text= \"" + text + "\""); 204 } 205 206 /** 207 * Performs a swipe Up on the UI element until the requested content-description 208 * is visible or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)} 209 * 210 * @param text to look for anywhere within the contents of this scrollable. 211 * @return true if item us found else false 212 * @since API Level 16 213 */ 214 public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException { 215 Tracer.trace(text); 216 return scrollIntoView(new UiSelector().description(text)); 217 } 218 219 /** 220 * Perform a scroll search for a UI element matching the {@link UiSelector} selector argument. 221 * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}. 222 * 223 * @param obj {@link UiObject} 224 * @return true if the item was found and now is in view else false 225 * @since API Level 16 226 */ 227 public boolean scrollIntoView(UiObject obj) throws UiObjectNotFoundException { 228 Tracer.trace(obj.getSelector()); 229 return scrollIntoView(obj.getSelector()); 230 } 231 232 /** 233 * Perform a scroll search for a UI element matching the {@link UiSelector} selector argument. 234 * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}. 235 * 236 * @param selector {@link UiSelector} selector 237 * @return true if the item was found and now is in view else false 238 * @since API Level 16 239 */ 240 public boolean scrollIntoView(UiSelector selector) throws UiObjectNotFoundException { 241 Tracer.trace(selector); 242 // if we happen to be on top of the text we want then return here 243 if (exists(getSelector().childSelector(selector))) { 244 return (true); 245 } else { 246 // we will need to reset the search from the beginning to start search 247 scrollToBeginning(mMaxSearchSwipes); 248 if (exists(getSelector().childSelector(selector))) { 249 return (true); 250 } 251 for (int x = 0; x < mMaxSearchSwipes; x++) { 252 if(!scrollForward()) { 253 return false; 254 } 255 256 if(exists(getSelector().childSelector(selector))) { 257 return true; 258 } 259 } 260 } 261 return false; 262 } 263 264 /** 265 * Performs a swipe up on the UI element until the requested text is visible 266 * or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)} 267 * 268 * @param text to look for 269 * @return true if item us found else false 270 * @since API Level 16 271 */ 272 public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException { 273 Tracer.trace(text); 274 return scrollIntoView(new UiSelector().text(text)); 275 } 276 277 /** 278 * {@link #getChildByDescription(UiSelector, String)} and 279 * {@link #getChildByText(UiSelector, String)} use an arguments that specifies if scrolling is 280 * allowed while searching for the UI element. The number of scrolls allowed to perform a 281 * search can be modified by this method. The current value can be read by calling 282 * {@link #getMaxSearchSwipes()} 283 * 284 * @param swipes is the number of search swipes until abort 285 * @return reference to itself 286 * @since API Level 16 287 */ 288 public UiScrollable setMaxSearchSwipes(int swipes) { 289 Tracer.trace(swipes); 290 mMaxSearchSwipes = swipes; 291 return this; 292 } 293 294 /** 295 * {@link #getChildByDescription(UiSelector, String)} and 296 * {@link #getChildByText(UiSelector, String)} use an arguments that specifies if scrolling is 297 * allowed while searching for the UI element. The number of scrolls currently allowed to 298 * perform a search can be read by this method. 299 * See {@link #setMaxSearchSwipes(int)} 300 * 301 * @return max value of the number of swipes currently allowed during a scroll search 302 * @since API Level 16 303 */ 304 public int getMaxSearchSwipes() { 305 Tracer.trace(); 306 return mMaxSearchSwipes; 307 } 308 309 /** 310 * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling 311 * 312 * @return true if scrolled and false if can't scroll anymore 313 * @since API Level 16 314 */ 315 public boolean flingForward() throws UiObjectNotFoundException { 316 Tracer.trace(); 317 return scrollForward(FLING_STEPS); 318 } 319 320 /** 321 * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a regular scroll 322 * 323 * @return true if scrolled and false if can't scroll anymore 324 * @since API Level 16 325 */ 326 public boolean scrollForward() throws UiObjectNotFoundException { 327 Tracer.trace(); 328 return scrollForward(SCROLL_STEPS); 329 } 330 331 /** 332 * Perform a scroll forward. If this list is set to vertical (see {@link #setAsVerticalList()} 333 * default) then the swipes will be executed from the bottom to top. If this list is set 334 * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from 335 * the right to left. Caution is required on devices configured with right to left languages 336 * like Arabic and Hebrew. 337 * 338 * @param steps use steps to control the speed, so that it may be a scroll, or fling 339 * @return true if scrolled and false if can't scroll anymore 340 * @since API Level 16 341 */ 342 public boolean scrollForward(int steps) throws UiObjectNotFoundException { 343 Tracer.trace(steps); 344 Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector()); 345 AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT); 346 if(node == null) { 347 throw new UiObjectNotFoundException(getSelector().toString()); 348 } 349 Rect rect = new Rect(); 350 node.getBoundsInScreen(rect); 351 352 int downX = 0; 353 int downY = 0; 354 int upX = 0; 355 int upY = 0; 356 357 // scrolling is by default assumed vertically unless the object is explicitly 358 // set otherwise by setAsHorizontalContainer() 359 if(mIsVerticalList) { 360 int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage()); 361 // scroll vertically: swipe down -> up 362 downX = rect.centerX(); 363 downY = rect.bottom - swipeAreaAdjust; 364 upX = rect.centerX(); 365 upY = rect.top + swipeAreaAdjust; 366 } else { 367 int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage()); 368 // scroll horizontally: swipe right -> left 369 // TODO: Assuming device is not in right to left language 370 downX = rect.right - swipeAreaAdjust; 371 downY = rect.centerY(); 372 upX = rect.left + swipeAreaAdjust; 373 upY = rect.centerY(); 374 } 375 return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps); 376 } 377 378 /** 379 * See {@link UiScrollable#scrollBackward(int)} 380 * 381 * @return true if scrolled and false if can't scroll anymore 382 * @since API Level 16 383 */ 384 public boolean flingBackward() throws UiObjectNotFoundException { 385 Tracer.trace(); 386 return scrollBackward(FLING_STEPS); 387 } 388 389 /** 390 * See {@link UiScrollable#scrollBackward(int)} 391 * 392 * @return true if scrolled and false if can't scroll anymore 393 * @since API Level 16 394 */ 395 public boolean scrollBackward() throws UiObjectNotFoundException { 396 Tracer.trace(); 397 return scrollBackward(SCROLL_STEPS); 398 } 399 400 /** 401 * Perform a scroll backward. If this list is set to vertical (see {@link #setAsVerticalList()} 402 * default) then the swipes will be executed from the top to bottom. If this list is set 403 * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from 404 * the left to right. Caution is required on devices configured with right to left languages 405 * like Arabic and Hebrew. 406 * 407 * @param steps use steps to control the speed, so that it may be a scroll, or fling 408 * @return true if scrolled and false if can't scroll anymore 409 * @since API Level 16 410 */ 411 public boolean scrollBackward(int steps) throws UiObjectNotFoundException { 412 Tracer.trace(steps); 413 Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector()); 414 AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT); 415 if (node == null) { 416 throw new UiObjectNotFoundException(getSelector().toString()); 417 } 418 Rect rect = new Rect(); 419 node.getBoundsInScreen(rect); 420 421 int downX = 0; 422 int downY = 0; 423 int upX = 0; 424 int upY = 0; 425 426 // scrolling is by default assumed vertically unless the object is explicitly 427 // set otherwise by setAsHorizontalContainer() 428 if(mIsVerticalList) { 429 int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage()); 430 Log.d(LOG_TAG, "scrollToBegining() using vertical scroll"); 431 // scroll vertically: swipe up -> down 432 downX = rect.centerX(); 433 downY = rect.top + swipeAreaAdjust; 434 upX = rect.centerX(); 435 upY = rect.bottom - swipeAreaAdjust; 436 } else { 437 int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage()); 438 Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll"); 439 // scroll horizontally: swipe left -> right 440 // TODO: Assuming device is not in right to left language 441 downX = rect.left + swipeAreaAdjust; 442 downY = rect.centerY(); 443 upX = rect.right - swipeAreaAdjust; 444 upY = rect.centerY(); 445 } 446 return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps); 447 } 448 449 /** 450 * Scrolls to the beginning of a scrollable UI element. The beginning could be the top most 451 * in case of vertical lists or the left most in case of horizontal lists. Caution is required 452 * on devices configured with right to left languages like Arabic and Hebrew. 453 * 454 * @param steps use steps to control the speed, so that it may be a scroll, or fling 455 * @return true on scrolled else false 456 * @since API Level 16 457 */ 458 public boolean scrollToBeginning(int maxSwipes, int steps) throws UiObjectNotFoundException { 459 Tracer.trace(maxSwipes, steps); 460 Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector()); 461 // protect against potential hanging and return after preset attempts 462 for(int x = 0; x < maxSwipes; x++) { 463 if(!scrollBackward(steps)) { 464 break; 465 } 466 } 467 return true; 468 } 469 470 /** 471 * See {@link UiScrollable#scrollToBeginning(int, int)} 472 * 473 * @param maxSwipes 474 * @return true on scrolled else false 475 * @since API Level 16 476 */ 477 public boolean scrollToBeginning(int maxSwipes) throws UiObjectNotFoundException { 478 Tracer.trace(maxSwipes); 479 return scrollToBeginning(maxSwipes, SCROLL_STEPS); 480 } 481 482 /** 483 * See {@link UiScrollable#scrollToBeginning(int, int)} 484 * 485 * @param maxSwipes 486 * @return true on scrolled else false 487 * @since API Level 16 488 */ 489 public boolean flingToBeginning(int maxSwipes) throws UiObjectNotFoundException { 490 Tracer.trace(maxSwipes); 491 return scrollToBeginning(maxSwipes, FLING_STEPS); 492 } 493 494 /** 495 * Scrolls to the end of a scrollable UI element. The end could be the bottom most 496 * in case of vertical controls or the right most for horizontal controls. Caution 497 * is required on devices configured with right to left languages like Arabic and Hebrew. 498 * 499 * @param steps use steps to control the speed, so that it may be a scroll, or fling 500 * @return true on scrolled else false 501 * @since API Level 16 502 */ 503 public boolean scrollToEnd(int maxSwipes, int steps) throws UiObjectNotFoundException { 504 Tracer.trace(maxSwipes, steps); 505 // protect against potential hanging and return after preset attempts 506 for(int x = 0; x < maxSwipes; x++) { 507 if(!scrollForward(steps)) { 508 break; 509 } 510 } 511 return true; 512 } 513 514 /** 515 * See {@link UiScrollable#scrollToEnd(int, int)} 516 * 517 * @param maxSwipes 518 * @return true on scrolled else false 519 * @since API Level 16 520 */ 521 public boolean scrollToEnd(int maxSwipes) throws UiObjectNotFoundException { 522 Tracer.trace(maxSwipes); 523 return scrollToEnd(maxSwipes, SCROLL_STEPS); 524 } 525 526 /** 527 * See {@link UiScrollable#scrollToEnd(int, int)} 528 * 529 * @param maxSwipes 530 * @return true on scrolled else false 531 * @since API Level 16 532 */ 533 public boolean flingToEnd(int maxSwipes) throws UiObjectNotFoundException { 534 Tracer.trace(maxSwipes); 535 return scrollToEnd(maxSwipes, FLING_STEPS); 536 } 537 538 /** 539 * Returns the percentage of a widget's size that's considered as no touch zone when swiping 540 * 541 * Dead zones are set as percentage of a widget's total width or height where 542 * swipe start point cannot start or swipe end point cannot end. It is like a margin 543 * around the actual dimensions of the widget. Swipes will always be start and 544 * end inside this margin. 545 * 546 * This is important when the widget being swiped may not respond to the swipe if 547 * started at a point too near to the edge. The default is 10% from either edge. 548 * 549 * @return a value between 0 and 1 550 * @since API Level 16 551 */ 552 public double getSwipeDeadZonePercentage() { 553 Tracer.trace(); 554 return mSwipeDeadZonePercentage; 555 } 556 557 /** 558 * Sets the percentage of a widget's size that's considered as no touch zone when swiping 559 * 560 * Dead zones are set as percentage of a widget's total width or height where 561 * swipe start point cannot start or swipe end point cannot end. It is like a margin 562 * around the actual dimensions of the widget. Swipes will always start and 563 * end inside this margin. 564 * 565 * This is important when the widget being swiped may not respond to the swipe if 566 * started at a point too near to the edge. The default is 10% from either edge 567 * 568 * @param swipeDeadZonePercentage is a value between 0 and 1 569 * @return reference to itself 570 * @since API Level 16 571 */ 572 public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) { 573 Tracer.trace(swipeDeadZonePercentage); 574 mSwipeDeadZonePercentage = swipeDeadZonePercentage; 575 return this; 576 } 577} 578