1"""Utils for adb-based UI operations."""
2
3import collections
4import logging
5import os
6import re
7import time
8
9from xml.dom import minidom
10from acts.controllers.android_lib.errors import AndroidDeviceError
11
12
13class Point(collections.namedtuple('Point', ['x', 'y'])):
14
15  def __repr__(self):
16    return '{x},{y}'.format(x=self.x, y=self.y)
17
18
19class Bounds(collections.namedtuple('Bounds', ['start', 'end'])):
20
21  def __repr__(self):
22    return '[{start}][{end}]'.format(start=str(self.start), end=str(self.end))
23
24  def calculate_middle_point(self):
25    return Point((self.start.x + self.end.x) // 2,
26                 (self.start.y + self.end.y) // 2)
27
28
29def get_key_value_pair_strings(kv_pairs):
30  return ' '.join(['%s="%s"' % (k, v) for k, v in kv_pairs.items()])
31
32
33def parse_bound(bounds_string):
34  """Parse UI bound string.
35
36  Args:
37    bounds_string: string, In the format of the UI element bound.
38                   e.g '[0,0][1080,2160]'
39
40  Returns:
41    Bounds, The bound of UI element.
42  """
43  bounds_pattern = re.compile(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]')
44  points = bounds_pattern.match(bounds_string).groups()
45  points = list(map(int, points))
46  return Bounds(Point(*points[:2]), Point(*points[-2:]))
47
48
49def _find_point_in_bounds(bounds_string):
50  """Finds a point that resides within the given bounds.
51
52  Args:
53    bounds_string: string, In the format of the UI element bound.
54
55  Returns:
56    A tuple of integers, representing X and Y coordinates of a point within
57    the given boundary.
58  """
59  return parse_bound(bounds_string).calculate_middle_point()
60
61
62def get_screen_dump_xml(device):
63  """Gets an XML dump of the current device screen.
64
65  This only works when there is no instrumentation process running. A running
66  instrumentation process will disrupt calls for `adb shell uiautomator dump`.
67
68  Args:
69    device: AndroidDevice object.
70
71  Returns:
72    XML Document of the screen dump.
73  """
74  os.makedirs(device.log_path, exist_ok=True)
75  device.adb.shell('uiautomator dump')
76  device.adb.pull('/sdcard/window_dump.xml %s' % device.log_path)
77  return minidom.parse('%s/window_dump.xml' % device.log_path)
78
79
80def match_node(node, **matcher):
81  """Determine if a mode matches with the given matcher.
82
83  Args:
84    node: Is a XML node to be checked against matcher.
85    **matcher: Is a dict representing mobly AdbUiDevice matchers.
86
87  Returns:
88    True if all matchers match the given node.
89  """
90  match_list = []
91  for k, v in matcher.items():
92    if k == 'class_name':
93      key = k.replace('class_name', 'class')
94    elif k == 'text_contains':
95      key = k.replace('text_contains', 'text')
96    else:
97      key = k.replace('_', '-')
98    try:
99      if k == 'text_contains':
100        match_list.append(v in node.attributes[key].value)
101      else:
102        match_list.append(node.attributes[key].value == v)
103    except KeyError:
104      match_list.append(False)
105  return all(match_list)
106
107
108def _find_node(screen_dump_xml, **kwargs):
109  """Finds an XML node from an XML DOM.
110
111  Args:
112    screen_dump_xml: XML doc, parsed from adb ui automator dump.
113    **kwargs: key/value pairs to match in an XML node's attributes. Value of
114      each key has to be string type. Below lists keys which can be used:
115        index
116        text
117        text_contains (matching a part of text attribute)
118        resource_id
119        class_name (representing "class" attribute)
120        package
121        content_desc
122        checkable
123        checked
124        clickable
125        enabled
126        focusable
127        focused
128        scrollable
129        long_clickable
130        password
131        selected
132
133  Returns:
134    XML node of the UI element or None if not found.
135  """
136  nodes = screen_dump_xml.getElementsByTagName('node')
137  for node in nodes:
138    if match_node(node, **kwargs):
139      logging.debug('Found a node matching conditions: %s',
140                    get_key_value_pair_strings(kwargs))
141      return node
142
143
144def wait_and_get_xml_node(device, timeout, child=None, sibling=None, **kwargs):
145  """Waits for a node to appear and return it.
146
147  Args:
148    device: AndroidDevice object.
149    timeout: float, The number of seconds to wait for before giving up.
150    child: dict, a dict contains child XML node's attributes. It is extra set of
151      conditions to match an XML node that is under the XML node which is found
152      by **kwargs.
153    sibling: dict, a dict contains sibling XML node's attributes. It is extra
154      set of conditions to match an XML node that is under parent of the XML
155      node which is found by **kwargs.
156    **kwargs: Key/value pairs to match in an XML node's attributes.
157
158  Returns:
159    The XML node of the UI element.
160
161  Raises:
162    AndroidDeviceError: if the UI element does not appear on screen within
163    timeout or extra sets of conditions of child and sibling are used in a call.
164  """
165  if child and sibling:
166    raise AndroidDeviceError(
167        device, 'Only use one extra set of conditions: child or sibling.')
168  start_time = time.time()
169  threshold = start_time + timeout
170  while time.time() < threshold:
171    time.sleep(1)
172    screen_dump_xml = get_screen_dump_xml(device)
173    node = _find_node(screen_dump_xml, **kwargs)
174    if node and child:
175      node = _find_node(node, **child)
176    if node and sibling:
177      node = _find_node(node.parentNode, **sibling)
178    if node:
179      return node
180  msg = ('Timed out after %ds waiting for UI node matching conditions: %s.'
181         % (timeout, get_key_value_pair_strings(kwargs)))
182  if child:
183    msg = ('%s extra conditions: %s'
184           % (msg, get_key_value_pair_strings(child)))
185  if sibling:
186    msg = ('%s extra conditions: %s'
187           % (msg, get_key_value_pair_strings(sibling)))
188  raise AndroidDeviceError(device, msg)
189
190
191def has_element(device, **kwargs):
192  """Checks a UI element whether appears or not in the current screen.
193
194  Args:
195    device: AndroidDevice object.
196    **kwargs: Key/value pairs to match in an XML node's attributes.
197
198  Returns:
199    True if the UI element appears in the current screen else False.
200  """
201  timeout_sec = kwargs.pop('timeout', 30)
202  try:
203    wait_and_get_xml_node(device, timeout_sec, **kwargs)
204    return True
205  except AndroidDeviceError:
206    return False
207
208
209def get_element_attributes(device, **kwargs):
210  """Gets a UI element's all attributes.
211
212  Args:
213    device: AndroidDevice object.
214    **kwargs: Key/value pairs to match in an XML node's attributes.
215
216  Returns:
217    XML Node Attributes.
218  """
219  timeout_sec = kwargs.pop('timeout', 30)
220  node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
221  return node.attributes
222
223
224def wait_and_click(device, duration_ms=None, **kwargs):
225  """Wait for a UI element to appear and click on it.
226
227  This function locates a UI element on the screen by matching attributes of
228  nodes in XML DOM, calculates a point's coordinates within the boundary of the
229  element, and clicks on the point marked by the coordinates.
230
231  Args:
232    device: AndroidDevice object.
233    duration_ms: int, The number of milliseconds to long-click.
234    **kwargs: A set of `key=value` parameters that identifies a UI element.
235  """
236  timeout_sec = kwargs.pop('timeout', 30)
237  button_node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
238  x, y = _find_point_in_bounds(button_node.attributes['bounds'].value)
239  args = []
240  if duration_ms is None:
241    args = 'input tap %s %s' % (str(x), str(y))
242  else:
243    # Long click.
244    args = 'input swipe %s %s %s %s %s' % \
245        (str(x), str(y), str(x), str(y), str(duration_ms))
246  device.adb.shell(args)
247