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