• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        A special key/value: matching_node key is used to identify If more than one nodes have the same key/value,
133            the matching_node stands for which matching node should be fetched.
134
135  Returns:
136    XML node of the UI element or None if not found.
137  """
138  nodes = screen_dump_xml.getElementsByTagName('node')
139  matching_node = kwargs.pop('matching_node', 1)
140  count = 1
141  for node in nodes:
142    if match_node(node, **kwargs):
143      if count == matching_node:
144        logging.debug('Found a node matching conditions: %s',
145                      get_key_value_pair_strings(kwargs))
146        return node
147      count += 1
148  return None
149
150
151def wait_and_get_xml_node(device, timeout, child=None, sibling=None, **kwargs):
152  """Waits for a node to appear and return it.
153
154  Args:
155    device: AndroidDevice object.
156    timeout: float, The number of seconds to wait for before giving up.
157    child: dict, a dict contains child XML node's attributes. It is extra set of
158      conditions to match an XML node that is under the XML node which is found
159      by **kwargs.
160    sibling: dict, a dict contains sibling XML node's attributes. It is extra
161      set of conditions to match an XML node that is under parent of the XML
162      node which is found by **kwargs.
163    **kwargs: Key/value pairs to match in an XML node's attributes.
164
165  Returns:
166    The XML node of the UI element.
167
168  Raises:
169    AndroidDeviceError: if the UI element does not appear on screen within
170    timeout or extra sets of conditions of child and sibling are used in a call.
171  """
172  if child and sibling:
173    raise AndroidDeviceError(
174        device, 'Only use one extra set of conditions: child or sibling.')
175  start_time = time.time()
176  threshold = start_time + timeout
177  while time.time() < threshold:
178    time.sleep(1)
179    screen_dump_xml = get_screen_dump_xml(device)
180    node = _find_node(screen_dump_xml, **kwargs)
181    if node and child:
182      node = _find_node(node, **child)
183    if node and sibling:
184      node = _find_node(node.parentNode, **sibling)
185    if node:
186      return node
187  msg = ('Timed out after %ds waiting for UI node matching conditions: %s.'
188         % (timeout, get_key_value_pair_strings(kwargs)))
189  if child:
190    msg = ('%s extra conditions: %s'
191           % (msg, get_key_value_pair_strings(child)))
192  if sibling:
193    msg = ('%s extra conditions: %s'
194           % (msg, get_key_value_pair_strings(sibling)))
195  raise AndroidDeviceError(device, msg)
196
197
198def has_element(device, **kwargs):
199  """Checks a UI element whether appears or not in the current screen.
200
201  Args:
202    device: AndroidDevice object.
203    **kwargs: Key/value pairs to match in an XML node's attributes.
204
205  Returns:
206    True if the UI element appears in the current screen else False.
207  """
208  timeout_sec = kwargs.pop('timeout', 30)
209  try:
210    wait_and_get_xml_node(device, timeout_sec, **kwargs)
211    return True
212  except AndroidDeviceError:
213    return False
214
215
216def get_element_attributes(device, **kwargs):
217  """Gets a UI element's all attributes.
218
219  Args:
220    device: AndroidDevice object.
221    **kwargs: Key/value pairs to match in an XML node's attributes.
222
223  Returns:
224    XML Node Attributes.
225  """
226  timeout_sec = kwargs.pop('timeout', 30)
227  node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
228  return node.attributes
229
230
231def wait_and_click(device, duration_ms=None, **kwargs):
232  """Wait for a UI element to appear and click on it.
233
234  This function locates a UI element on the screen by matching attributes of
235  nodes in XML DOM, calculates a point's coordinates within the boundary of the
236  element, and clicks on the point marked by the coordinates.
237
238  Args:
239    device: AndroidDevice object.
240    duration_ms: int, The number of milliseconds to long-click.
241    **kwargs: A set of `key=value` parameters that identifies a UI element.
242  """
243  timeout_sec = kwargs.pop('timeout', 30)
244  button_node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
245  x, y = _find_point_in_bounds(button_node.attributes['bounds'].value)
246  args = []
247  if duration_ms is None:
248    args = 'input tap %s %s' % (str(x), str(y))
249  else:
250    # Long click.
251    args = 'input swipe %s %s %s %s %s' % \
252        (str(x), str(y), str(x), str(y), str(duration_ms))
253  device.adb.shell(args)
254
255def wait_and_input_text(device, input_text, duration_ms=None, **kwargs):
256  """Wait for a UI element text field that can accept text entry.
257
258  This function located a UI element using wait_and_click. Once the element is
259  clicked, the text is input into the text field.
260
261  Args:
262    device: AndroidDevice, Mobly's Android controller object.
263    input_text: Text string to be entered in to the text field.
264    duration_ms: duration in milliseconds.
265    **kwargs: A set of `key=value` parameters that identifies a UI element.
266  """
267  wait_and_click(device, duration_ms, **kwargs)
268  # Replace special characters.
269  # The command "input text <string>" requires special treatment for
270  # characters ' ' and '&'.  They need to be escaped. for example:
271  #    "hello world!!&" needs to transform to "hello\ world!!\&"
272  special_chars = ' &'
273  for c in special_chars:
274    input_text = input_text.replace(c, '\\%s' % c)
275  input_text = "'" + input_text + "'"
276  args = 'input text %s' % input_text
277  device.adb.shell(args)
278