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