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