1"""Core components of ui_pages.""" 2from __future__ import annotations 3 4import abc 5import itertools 6import logging 7import os 8import re 9import shlex 10import time 11from typing import Any, Callable, Dict, Generator, Iterable, List, Sequence, NamedTuple, Optional, Tuple, Type 12from xml.dom import minidom 13 14from mobly.controllers import android_device 15from mobly.controllers.android_device_lib import adb 16 17# Internal import 18from blueberry.utils.ui_pages import errors 19from blueberry.utils.ui_pages import ui_node 20from blueberry.utils.ui_pages import utils 21 22# Return type of otpional UINode. 23OptUINode = Optional[ui_node.UINode] 24 25# Return type of node generator. 26NodeGenerator = Generator[ui_node.UINode, None, None] 27 28# Waiting time of expecting page. 29_EXPECT_PAGE_WAIT_TIME_IN_SECOND = 20 30 31# Function to evaluate UINode. 32NodeEvaluator = Optional[Callable[[ui_node.UINode], bool]] 33 34# Number of retries in retrieving UI xml file. 35_RETRIES_NUM_OF_ADB_COMMAND = 5 36 37 38# Dataclass for return of UI parsing result. 39class ParsedUI(NamedTuple): 40 ui_xml: minidom.Document 41 clickable_nodes: List[ui_node.UINode] 42 enabled_nodes: List[ui_node.UINode] 43 all_nodes: List[ui_node.UINode] 44 45 46class Context(abc.ABC): 47 """Context of UI page. 48 49 Attributes: 50 ad: The Android device where the UI pages are derived from. 51 page: The last obtained UI page object. 52 regr_page_calls: Key as page class; value as registered method of page class 53 to call when meeting it. 54 root_node: The root node of parsed XML file dumped from adb. 55 known_pages: List of UIPage objects used to represent the current page. 56 log: The logger object. 57 safe_get: The method `safe_get_page` will be used to get page iff True. 58 Otherwise the method `strict_get_page` will be used instead as default. 59 enable_registered_page_call: True to enable the phrase of executing 60 registered page action(s). It is used to avoid infinite loop situations. 61 """ 62 63 def __init__(self, 64 ad: android_device.AndroidDevice, 65 known_pages: List[Type[UIPage]], 66 do_go_home: bool = True, 67 safe_get: bool = False) -> None: 68 self.log = logging.getLogger(self.__class__.__name__) 69 self.ad = ad 70 self.page = None 71 self.regr_page_calls = {} 72 self.root_node = None 73 self.known_pages = known_pages 74 self.safe_get = safe_get 75 self.enable_registered_page_call = True 76 self.log.debug('safe_get=%s; do_go_home=%s', self.safe_get, do_go_home) 77 if do_go_home: 78 self.unlock_screen() 79 self.go_home_page() 80 else: 81 self.get_page() 82 83 def regr_page_call(self, page_class: Type[UIPage], method_name: str) -> None: 84 """Registers a page call. 85 86 This method is used to register a fixed method call on registered 87 page class. Whenever the current page is of regisered page class, 88 the registered method name will be called automatically. 89 90 Args: 91 page_class: The page class to register for callback. 92 method_name: Name of method from `page_class` to be registered. 93 """ 94 self.regr_page_calls[page_class] = method_name 95 96 def get_regr_page_call(self, page_obj: UIPage) -> Optional[str]: 97 """Gets the registered method name. 98 99 We use method `self.regr_page_call` to register the subclass of UIPage 100 as specific page and its method name. Then this method is used to 101 retrieve the registered method name according to the input page object. 102 103 Args: 104 page_obj: The page object to search for registered method name. 105 106 Returns: 107 The registered method name of given page object iff registered. 108 Otherwise, None is returned. 109 """ 110 for page_class, method_name in self.regr_page_calls.items(): 111 if isinstance(page_obj, page_class): 112 return method_name 113 114 return None 115 116 @abc.abstractmethod 117 def go_home_page(self) -> UIPage: 118 """Goes to home page. 119 120 This is a abtract method to be implemented in subclass. 121 Different App will have different home page and this method 122 is implemented in the context of each App. 123 124 Returns: 125 The home page object. 126 """ 127 pass 128 129 def go_page(self, page_class: Type[UIPage]) -> UIPage: 130 """Goes to target page. 131 132 Args: 133 page_class: The class of target page to go to. 134 135 Returns: 136 The corresponding UIPage of given page class. 137 138 Raises: 139 errors.ContextError: Fail to reach target page. 140 """ 141 if self.is_page(page_class): 142 return self.page 143 144 self.ad.adb.shell(f'am start -n {page_class.ACTIVITY}') 145 self.get_page() 146 self.expect_page(page_class) 147 148 return self.page 149 150 def send_keycode(self, keycode: str) -> None: 151 """Sends keycode. 152 153 Args: 154 keycode: Key code to be sent. e.g.: "BACK" 155 """ 156 self.ad.adb.shell(f'input keyevent KEYCODE_{keycode}') 157 158 def back(self) -> UIPage: 159 """Sends keycode 'BACK'. 160 161 Returns: 162 The transformed page object. 163 """ 164 self.send_keycode('BACK') 165 return self.get_page() 166 167 def send_keycode_number_pad(self, number: str) -> None: 168 """Sends keycode of number pad. 169 170 Args: 171 number: The number pad to be sent. 172 """ 173 self.send_keycode(f'NUMPAD_{number}') 174 175 def get_my_current_focus_app(self) -> str: 176 """Gets the current focus application. 177 178 Returns: 179 The current focus app activity name iff it works. 180 Otherwise, empty string is returned. 181 """ 182 output = self.ad.adb.shell( 183 'dumpsys activity | grep -E mFocusedApp').decode() 184 if any([ 185 not output, 'not found' in output, "Can't find" in output, 186 'mFocusedApp=null' in output 187 ]): 188 self.log.warning( 189 'Fail to obtain the current app activity with output: %s', output) 190 return '' 191 192 # The output may look like: 193 # ActivityRecord{... FitbitMobile/com.fitbit.home.ui.HomeActivity t93} 194 # and we want to extract 'FitbitMobile/com.fitbit.home.ui.HomeActivity' 195 result = output.split(' ')[-2] 196 self.log.debug('Current focus app activity is %s', result) 197 return result 198 199 def unlock_screen(self) -> UIPage: 200 """Unlocks the screen. 201 202 This method will assume that the device is not protected 203 by password under testing. 204 205 Returns: 206 The page object after unlock. 207 """ 208 # Bring device to SLEEP so that unlock process can start fresh. 209 self.send_keycode('SLEEP') 210 time.sleep(1) 211 self.send_keycode('WAKEUP') 212 self.get_page() 213 self.page.swipe_down() 214 return self.page 215 216 def is_page(self, page_class: Type[UIPage]) -> bool: 217 """Checks the current page is of expected page. 218 219 Args: 220 page_class: The class of expected page. 221 222 Returns: 223 True iff the current page is of expected page. 224 """ 225 return isinstance(self.page, page_class) 226 227 @retry.logged_retry_on_exception( 228 retry_value=(adb.Error, errors.ContextError), 229 retry_intervals=retry.FuzzedExponentialIntervals( 230 initial_delay_sec=1, 231 num_retries=_RETRIES_NUM_OF_ADB_COMMAND, 232 factor=1.1)) 233 def get_ui_xml(self, xml_out_dir: str = '/tmp/') -> minidom.Document: 234 """Gets the XML object of current UI. 235 236 Args: 237 xml_out_dir: The host directory path to store the dumped UI XML file. 238 239 Returns: 240 The parsed XML object of current UI page. 241 242 Raises: 243 errors.ContextError: Fail to dump UI xml from adb. 244 """ 245 # Clean exist dump xml file in host if any to avoid 246 # parsing the previous dumped xml file. 247 dump_xml_name = 'window_dump.xml' 248 xml_path = os.path.join(xml_out_dir, dump_xml_name) 249 if os.path.isfile(xml_path): 250 os.remove(xml_path) 251 252 dump_xml_path = f'/sdcard/{dump_xml_name}' 253 self.ad.adb.shell( 254 f"test -f {dump_xml_path} && rm {dump_xml_path} || echo 'no dump xml'") 255 self.ad.adb.shell('uiautomator dump') 256 self.ad.adb.pull(shlex.split(f'{dump_xml_path} {xml_out_dir}')) 257 258 if not os.path.isfile(xml_path): 259 raise errors.ContextError(self, f'Fail to dump UI xml to {xml_path}!') 260 261 return minidom.parse(xml_path) 262 263 def parse_ui(self, xml_path: Optional[str] = None) -> ParsedUI: 264 """Parses the current UI page. 265 266 Args: 267 xml_path: Target XML file path. If this argument is given, this method 268 will parse this XML file instead of dumping XML file through adb. 269 270 Returns: 271 Parsed tuple as [ 272 <UI XML object>, 273 <List of clickable nodes>, 274 <List of enabled nodes>, 275 ] 276 277 Raises: 278 errors.ContextError: Fail to dump UI XML from adb. 279 """ 280 if xml_path and os.path.isfile(xml_path): 281 ui_xml = minidom.parse(xml_path) 282 else: 283 ui_xml = self.get_ui_xml() 284 285 root = ui_xml.documentElement 286 self.root_node = ui_node.UINode(root.childNodes[0]) 287 288 clickable_nodes = [] 289 enabled_nodes = [] 290 all_nodes = [] 291 292 # TODO(user): Avoid the usage of nested functions. 293 def _get_node_attribute(node: ui_node.UINode, name: str) -> Optional[str]: 294 """Gets the attribute of give node by name if exist.""" 295 attribute = node.attributes.get(name) 296 if attribute: 297 return attribute.value 298 else: 299 return None 300 301 def _search_node(node: ui_node.UINode) -> None: 302 """Searches node(s) with desired attribute name to be true by DFS. 303 304 Args: 305 node: The current node to process. 306 """ 307 rid = node.resource_id.strip() 308 clz = node.clz.strip() 309 310 if rid or clz: 311 if _get_node_attribute(node, 'clickable') == 'true': 312 clickable_nodes.append(node) 313 if _get_node_attribute(node, 'enabled') == 'true': 314 enabled_nodes.append(node) 315 316 all_nodes.append(node) 317 318 for child_node in node.child_nodes: 319 _search_node(child_node) 320 321 # TODO(user): Store nodes information in a dataclass. 322 _search_node(self.root_node) 323 return ParsedUI(ui_xml, clickable_nodes, enabled_nodes, all_nodes) 324 325 def expect_pages(self, 326 page_class_list: Sequence[Type[UIPage]], 327 wait_sec: int = _EXPECT_PAGE_WAIT_TIME_IN_SECOND, 328 node_eval: NodeEvaluator = None) -> None: 329 """Waits for expected pages for certain time. 330 331 Args: 332 page_class_list: The list of expected page class. 333 wait_sec: The waiting time. 334 node_eval: Function to search the node. Here it is used to confirm that 335 the target page contain the desired node. 336 337 Raises: 338 errors.ContextError: Fail to reach expected page. 339 """ 340 end_time = time.monotonic() + wait_sec 341 while time.monotonic() < end_time: 342 page = self.safe_get_page() 343 if any((isinstance(page, page_class) for page_class in page_class_list)): 344 if node_eval is not None and page.get_node_by_func(node_eval) is None: 345 continue 346 347 return 348 349 raise errors.ContextError( 350 self, 351 f'Fail to reach page(s): {page_class_list} (current page={page})!') 352 353 def expect_page(self, 354 page_class: Type[UIPage], 355 wait_sec: int = _EXPECT_PAGE_WAIT_TIME_IN_SECOND, 356 node_eval: NodeEvaluator = None) -> None: 357 """Waits for expected page for certain time.""" 358 self.expect_pages([page_class], wait_sec=wait_sec, node_eval=node_eval) 359 360 def get_page(self, 361 wait_sec: int = 1, 362 xml_path: Optional[str] = None) -> UIPage: 363 if self.safe_get: 364 return self.safe_get_page(wait_sec=wait_sec, xml_path=xml_path) 365 else: 366 return self.strict_get_page(wait_sec=wait_sec, xml_path=xml_path) 367 368 def safe_get_page(self, 369 wait_sec: int = 1, 370 xml_path: Optional[str] = None) -> UIPage: 371 """Gets the represented UIPage object of current UI page safely. 372 373 Args: 374 wait_sec: Wait in second before actions. 375 xml_path: Target XML file path. If this argument is given, this method 376 will parse this XML file instead of dumping XML file from adb. 377 378 Returns: 379 The focused UIPage object will be returned iff the XML object can 380 be obtained and recognized. Otherwise, NonePage is returned. 381 """ 382 try: 383 self.strict_get_page(wait_sec=wait_sec, xml_path=xml_path) 384 except errors.UnknownPageError as err: 385 self.ad.log.warning(str(err)) 386 self.page = NonePage( 387 self, 388 ui_xml=err.ui_xml, 389 clickable_nodes=err.clickable_nodes, 390 enabled_nodes=err.enabled_nodes, 391 all_nodes=err.all_nodes) 392 393 return self.page 394 395 def strict_get_page(self, 396 wait_sec: int = 1, 397 xml_path: Optional[str] = None) -> Optional[UIPage]: 398 """Gets the represented UIPage object of current UI page. 399 400 This method will use adb command to dump UI XML file into local and use 401 the content of the dumped XML file to decide the proper page class and 402 instantiate it to self.page 403 404 Args: 405 wait_sec: Wait in second before actions. 406 xml_path: Target XML file path. If this argument is given, this method 407 will parse this XML file instead of dumping XML file from adb. 408 409 Returns: 410 The focused UIPage object will be returned iff the XML object can 411 be obtained and recognized. 412 413 Raises: 414 errors.UnknownPageError: Fail to recognize the content of 415 current UI page. 416 errors.ContextError: Fail to dump UI xml file. 417 """ 418 time.sleep(wait_sec) 419 ui_xml, clickable_nodes, enabled_nodes, all_nodes = self.parse_ui(xml_path) 420 421 for page_class in self.known_pages: 422 page_obj = page_class.from_xml(self, ui_xml, clickable_nodes, 423 enabled_nodes, all_nodes) 424 425 if page_obj: 426 if self.page is not None and isinstance(page_obj, self.page.__class__): 427 self.log.debug('Refreshing page %s...', self.page) 428 self.page.refresh(page_obj) 429 else: 430 self.page = page_obj 431 432 if self.enable_registered_page_call: 433 regr_method_name = self.get_regr_page_call(self.page) 434 if regr_method_name: 435 return getattr(self.page, regr_method_name)() 436 437 return self.page 438 439 raise errors.UnknownPageError(ui_xml, clickable_nodes, enabled_nodes, 440 all_nodes) 441 442 def get_display_size(self) -> Tuple[int, int]: 443 """Gets the display size of the device. 444 445 Returns: 446 tuple(width, height) of the display size. 447 448 Raises: 449 errors.ContextError: Obtained unexpected output of 450 display size from adb. 451 """ 452 # e.g.: Physical size: 384x384 453 output = self.ad.adb.shell(shlex.split('wm size')).decode() 454 size_items = output.rsplit(' ', 1)[-1].split('x') 455 if len(size_items) == 2: 456 return (int(size_items[0]), int(size_items[1])) 457 458 raise errors.ContextError(self, f'Illegal output of display size: {output}') 459 460 461class UIPage: 462 """Object to represent the current UI page. 463 464 Attributes: 465 ctx: The context object to hold the current page. 466 ui_xml: Parsed XML object. 467 clickable_nodes: List of UINode with attribute `clickable="true"` 468 enabled_nodes: List of UINode with attribute `enabled="true"` 469 all_nodes: List of all UINode 470 log: Logger object. 471 """ 472 473 # Defined in subclass 474 ACTIVITY = None 475 476 # Defined in subclass 477 PAGE_RID = None 478 479 # Defined in subclass 480 PAGE_TEXT = None 481 482 # Defined in subclass 483 PAGE_RE_TEXT = None 484 485 # Defined in subclass 486 PAGE_TITLE = None 487 488 def __init__(self, ctx: Context, ui_xml: Optional[minidom.Document], 489 clickable_nodes: List[ui_node.UINode], 490 enabled_nodes: List[ui_node.UINode], 491 all_nodes: List[ui_node.UINode]) -> None: 492 self.ctx = ctx 493 self.ui_xml = ui_xml 494 self.clickable_nodes = clickable_nodes 495 self.enabled_nodes = enabled_nodes 496 self.all_nodes = all_nodes 497 self.log = logging.getLogger(self.__class__.__name__) 498 499 @classmethod 500 def from_xml(cls, ctx: Context, ui_xml: minidom.Document, 501 clickable_nodes: List[ui_node.UINode], 502 enabled_nodes: List[ui_node.UINode], 503 all_nodes: List[ui_node.UINode]) -> Optional[UIPage]: 504 """Instantiates page object from XML object. 505 506 Args: 507 ctx: Page context object. 508 ui_xml: Parsed XML object. 509 clickable_nodes: Clickable node list from page. 510 enabled_nodes: Enabled node list from page. 511 all_nodes: All node list from the page. 512 513 Returns: 514 UI page object iff the given XML object can be parsed. 515 516 Raises: 517 errors.UIError: The page class doesn't provide signature 518 for matching. 519 """ 520 if cls.PAGE_RID is not None: 521 for node in enabled_nodes + clickable_nodes: 522 if node.resource_id == cls.PAGE_RID: 523 return cls(ctx, ui_xml, clickable_nodes, enabled_nodes, all_nodes) 524 elif cls.PAGE_TEXT is not None: 525 for node in enabled_nodes + clickable_nodes: 526 if node.text == cls.PAGE_TEXT: 527 return cls(ctx, ui_xml, clickable_nodes, enabled_nodes, all_nodes) 528 elif cls.PAGE_RE_TEXT is not None: 529 for node in enabled_nodes + clickable_nodes: 530 if re.search(cls.PAGE_RE_TEXT, node.text): 531 return cls(ctx, ui_xml, clickable_nodes, enabled_nodes, all_nodes) 532 elif cls.PAGE_TITLE is not None: 533 for node in enabled_nodes + clickable_nodes: 534 if all([ 535 node.resource_id == 'android:id/title', node.text == cls.PAGE_TITLE 536 ]): 537 return cls(ctx, ui_xml, clickable_nodes, enabled_nodes, all_nodes) 538 else: 539 raise errors.UIError(f'Illegal UI Page class: {cls}') 540 541 def refresh(self, new_page: UIPage) -> UIPage: 542 """Refreshes current page with obtained latest page. 543 544 Args: 545 new_page: The page with latest data for current page to be refreshed. 546 547 Returns: 548 The current refreshed UI page object. 549 """ 550 self.ui_xml = new_page.ui_xml 551 self.clickable_nodes = new_page.clickable_nodes 552 self.enabled_nodes = new_page.enabled_nodes 553 return self 554 555 def _get_node_search_space( 556 self, from_all: bool = False) -> Iterable[ui_node.UINode]: 557 """Gets the search space of node.""" 558 if from_all: 559 return self.all_nodes 560 else: 561 return itertools.chain(self.clickable_nodes, self.enabled_nodes) 562 563 def get_node_by_content_desc(self, 564 content_desc: str, 565 from_all: bool = False) -> OptUINode: 566 """Gets the first node with desired content description. 567 568 Args: 569 content_desc: Content description used for search. 570 from_all: True to search from all nodes; False to search only the 571 clickable or enabled nodes. 572 573 Returns: 574 Return the first node found with expected content description 575 iff it exists. Otherwise, None is returned. 576 """ 577 # TODO(user): Redesign APIs for intuitive usage. 578 for node in self._get_node_search_space(from_all): 579 if node.content_desc == content_desc: 580 return node 581 582 return None 583 584 def get_node_by_func(self, 585 func: Callable[[ui_node.UINode], bool], 586 from_all: bool = False) -> OptUINode: 587 """Gets the first node found by given function. 588 589 Args: 590 func: The function to search target node. 591 from_all: True to search from all nodes; False to search only the 592 clickable or enabled nodes. 593 594 Returns: 595 The node found by given function. 596 """ 597 for node in self._get_node_search_space(from_all): 598 if func(node): 599 return node 600 601 return None 602 603 def get_node_by_text(self, text: str, from_all: bool = False) -> OptUINode: 604 """Gets the first node with desired text. 605 606 Args: 607 text: Text used for search. 608 from_all: True to search from all nodes; False to search only the 609 clickable or enabled nodes. 610 611 Returns: 612 Return the first node found with expected text iff it exists. 613 Otherwise, None is returned. 614 """ 615 for node in self._get_node_search_space(from_all): 616 if node.text == text: 617 return node 618 619 return None 620 621 def _yield_node_by_rid(self, 622 rid: str, 623 from_all: bool = False) -> NodeGenerator: 624 """Generates node with desired resource id.""" 625 for node in self._get_node_search_space(from_all): 626 if ('resource-id' in node.attributes and 627 node.attributes['resource-id'].value == rid): 628 yield node 629 630 def get_all_nodes_by_rid(self, 631 rid: str, 632 from_all: bool = False) -> List[ui_node.UINode]: 633 """Gets all nodes with desired resource id. 634 635 Args: 636 rid: Resource id used for search. 637 from_all: True to search from all nodes; False to search only the 638 clickable or enabled nodes. 639 640 Returns: 641 The list of nodes found with expected resource id. 642 """ 643 found_node_set = set(self._yield_node_by_rid(rid, from_all)) 644 return list(found_node_set) 645 646 def get_node_by_rid(self, 647 rid: str, 648 from_all: bool = False) -> Optional[ui_node.UINode]: 649 """Gets the first node with desired resource id. 650 651 Args: 652 rid: Resource id used for search. 653 from_all: True to search from all nodes; False to search only the 654 clickable or enabled nodes. 655 656 Returns: 657 Return the first node found with expected resource id iff it exists. 658 Otherwise, None. 659 """ 660 try: 661 return next(self._yield_node_by_rid(rid, from_all)) 662 except StopIteration: 663 return None 664 665 def get_node_by_class(self, 666 class_name: str, 667 from_all: bool = False) -> Optional[ui_node.UINode]: 668 """Gets the first node with desired class. 669 670 Args: 671 class_name: Name of class as attribute. 672 from_all: True to search from all nodes; False to search only the 673 clickable or enabled nodes. 674 675 Returns: 676 Return the first node found with desired class iff it exists. 677 Otherwise, None. 678 """ 679 for node in self._get_node_search_space(from_all): 680 if node.clz == class_name: 681 return node 682 683 return None 684 685 def get_node_by_attrs(self, 686 attrs: Dict[str, Any], 687 from_all: bool = False) -> OptUINode: 688 """Gets the first node with the given attributes. 689 690 Args: 691 attrs: Attributes used to search target node. 692 from_all: True to search from all nodes; False to search only the 693 clickable or enabled nodes. 694 695 Returns: 696 Return the first UI node with expected attributes iff it exists. 697 Otherwise, None is returned. 698 """ 699 for node in self._get_node_search_space(from_all): 700 if node.match_attrs(attrs): 701 return node 702 703 return None 704 705 @utils.dr_wakeup_before_op 706 def swipe(self, 707 start_x: int, 708 start_y: int, 709 end_x: int, 710 end_y: int, 711 duration_ms: int, 712 swipes: int = 1) -> UIPage: 713 """Performs the swipe from one coordinate to another coordinate. 714 715 Args: 716 start_x: The starting X-axis coordinate. 717 start_y: The starting Y-axis coordinate. 718 end_x: The ending X-axis coordinate. 719 end_y: The ending Y-axis coordinate. 720 duration_ms: The millisecond of duration to drag. 721 swipes: How many swipe to carry on. 722 723 Returns: 724 The transformed UI page. 725 """ 726 for _ in range(swipes): 727 self.ctx.ad.adb.shell( 728 shlex.split( 729 f'input swipe {start_x} {start_y} {end_x} {end_y} {duration_ms}')) 730 731 return self.ctx.get_page() 732 733 def swipe_left(self, 734 duration_ms: int = 1000, 735 x_start: float = 0.2, 736 x_end: float = 0.9, 737 swipes: int = 1) -> UIPage: 738 """Performs the swipe left action. 739 740 Args: 741 duration_ms: Number of milliseconds to swipe from start point to end 742 point. 743 x_start: The range of width as start position 744 x_end: The range of width as end position 745 swipes: Round to conduct the swipe action. 746 747 Returns: 748 The transformed UI page. 749 """ 750 width, height = self.ctx.get_display_size() 751 self.log.info('Page size=(%d, %d)', width, height) 752 return self.swipe( 753 width * x_start, 754 height * 0.5, 755 width * x_end, 756 height * 0.5, 757 duration_ms=duration_ms, 758 swipes=swipes) 759 760 def swipe_right(self, 761 duration_ms: int = 1000, 762 x_start: float = 0.9, 763 x_end: float = 0.2, 764 swipes: int = 1) -> UIPage: 765 """Performs the swipe right action. 766 767 Args: 768 duration_ms: Number of milliseconds to swipe from start point to end 769 point. 770 x_start: The range of width as start position 771 x_end: The range of width as end position 772 swipes: Round to conduct the swipe action. 773 774 Returns: 775 The transformed UI page. 776 """ 777 width, height = self.ctx.get_display_size() 778 return self.swipe( 779 width * x_start, 780 height * 0.5, 781 width * x_end, 782 height * 0.5, 783 duration_ms=duration_ms, 784 swipes=swipes) 785 786 def swipe_down(self, duration_ms: int = 1000, swipes: int = 1) -> UIPage: 787 """Performs the swipe down action. 788 789 Args: 790 duration_ms: Number of milliseconds to swipe from start point to end 791 point. 792 swipes: Round to conduct the swipe action. 793 794 Returns: 795 The transformed UI page. 796 """ 797 width, height = self.ctx.get_display_size() 798 return self.swipe( 799 width * 0.5, 800 height * 0.7, 801 width * 0.5, 802 height * 0.2, 803 duration_ms=duration_ms, 804 swipes=swipes) 805 806 def swipe_up(self, duration_ms: int = 1000, swipes: int = 1) -> UIPage: 807 """Performs the swipe up action. 808 809 Args: 810 duration_ms: Number of milliseconds to swipe from start point to end 811 point. 812 swipes: Round to conduct the swipe action. 813 814 Returns: 815 The transformed UI page. 816 """ 817 width, height = self.ctx.get_display_size() 818 return self.swipe( 819 width * 0.5, 820 height * 0.2, 821 width * 0.5, 822 height * 0.7, 823 duration_ms=duration_ms, 824 swipes=swipes) 825 826 def click_on(self, x: int, y: int) -> None: 827 """Clicks on the given X/Y coordinates. 828 829 Args: 830 x: X-axis coordinate 831 y: Y-axis coordinate 832 833 Raises: 834 acts.controllers.adb.AdbError: If the adb shell command 835 failed to execute. 836 """ 837 self.ctx.ad.adb.shell(shlex.split(f'input tap {x} {y}')) 838 839 @utils.dr_wakeup_before_op 840 def click(self, 841 node: ui_node.UINode, 842 do_get_page: bool = True) -> Optional[UIPage]: 843 """Clicks the given UI node. 844 845 Args: 846 node: Node to click on. 847 do_get_page: Gets the latest page after clicking iff True. 848 849 Returns: 850 The transformed UI page is returned iff `do_get_page` is True. 851 Otherwise, None is returned. 852 """ 853 self.click_on(node.x, node.y) 854 if do_get_page: 855 return self.ctx.get_page() 856 857 def click_node_by_rid(self, 858 node_rid: str, 859 do_get_page: bool = True) -> UIPage: 860 """Clicks on node its resource id. 861 862 Args: 863 node_rid: Resource ID of node to search and click on. 864 do_get_page: Gets the latest page after clicking iff True. 865 866 Returns: 867 The transformed page. 868 869 Raises: 870 errors.UIError: Fail to get target node. 871 """ 872 node = self.get_node_by_rid(node_rid) 873 if node is None: 874 raise errors.UIError(f'Fail to find the node with resource id={node_rid}') 875 876 return self.click(node, do_get_page) 877 878 def click_node_by_text(self, text: str, do_get_page: bool = True) -> UIPage: 879 """Clicks on node by its text. 880 881 Args: 882 text: Text of node to search and click on. 883 do_get_page: Gets the latest page after clicking iff True. 884 885 Returns: 886 The transformed page. 887 888 Raises: 889 errors.UIError: Fail to get target node. 890 """ 891 node = self.get_node_by_text(text) 892 if node is None: 893 raise errors.UIError(f'Fail to find the node with text={text}') 894 895 return self.click(node, do_get_page) 896 897 def click_node_by_content_desc(self, 898 text: str, 899 do_get_page: bool = True) -> UIPage: 900 """Clicks on node by its content description. 901 902 Args: 903 text: Content description of node to search and click on. 904 do_get_page: Gets the latest page after clicking iff True. 905 906 Returns: 907 The transformed page. 908 909 Raises: 910 errors.UIError: Fail to get target node. 911 """ 912 node = self.get_node_by_content_desc(text) 913 if node is None: 914 raise errors.UIError( 915 f'Fail to find the node with content description={text}') 916 917 return self.click(node, do_get_page) 918 919 def click_node_by_class(self, 920 class_value: str, 921 do_get_page: bool = True) -> UIPage: 922 """Clicks on node by its class attribute value. 923 924 Args: 925 class_value: Value of class attribute. 926 do_get_page: Gets the latest page after clicking iff True. 927 928 Returns: 929 The transformed page. 930 931 Raises: 932 errors.UIError: Fail to get target node. 933 """ 934 node = self.get_node_by_class(class_value) 935 if node is None: 936 raise errors.UIError(f'Fail to find the node with class={class_value}') 937 938 return self.click(node, do_get_page) 939 940 941class NonePage(UIPage): 942 """None page to handle the context when we fail to dump UI xml file.""" 943