1# Copyright 2013-2015 ARM Limited 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# 15 16 17""" 18Utility functions for working with Android devices through adb. 19 20""" 21# pylint: disable=E1103 22import os 23import time 24import subprocess 25import logging 26import re 27from collections import defaultdict 28 29from devlib.exception import TargetError, HostError, DevlibError 30from devlib.utils.misc import check_output, which, memoized 31from devlib.utils.misc import escape_single_quotes, escape_double_quotes 32 33 34logger = logging.getLogger('android') 35 36MAX_ATTEMPTS = 5 37AM_START_ERROR = re.compile(r"Error: Activity.*") 38 39# See: 40# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels 41ANDROID_VERSION_MAP = { 42 23: 'MARSHMALLOW', 43 22: 'LOLLYPOP_MR1', 44 21: 'LOLLYPOP', 45 20: 'KITKAT_WATCH', 46 19: 'KITKAT', 47 18: 'JELLY_BEAN_MR2', 48 17: 'JELLY_BEAN_MR1', 49 16: 'JELLY_BEAN', 50 15: 'ICE_CREAM_SANDWICH_MR1', 51 14: 'ICE_CREAM_SANDWICH', 52 13: 'HONEYCOMB_MR2', 53 12: 'HONEYCOMB_MR1', 54 11: 'HONEYCOMB', 55 10: 'GINGERBREAD_MR1', 56 9: 'GINGERBREAD', 57 8: 'FROYO', 58 7: 'ECLAIR_MR1', 59 6: 'ECLAIR_0_1', 60 5: 'ECLAIR', 61 4: 'DONUT', 62 3: 'CUPCAKE', 63 2: 'BASE_1_1', 64 1: 'BASE', 65} 66 67 68# Initialized in functions near the botton of the file 69android_home = None 70platform_tools = None 71adb = None 72aapt = None 73fastboot = None 74 75 76class AndroidProperties(object): 77 78 def __init__(self, text): 79 self._properties = {} 80 self.parse(text) 81 82 def parse(self, text): 83 self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text)) 84 85 def iteritems(self): 86 return self._properties.iteritems() 87 88 def __iter__(self): 89 return iter(self._properties) 90 91 def __getattr__(self, name): 92 return self._properties.get(name) 93 94 __getitem__ = __getattr__ 95 96 97class AdbDevice(object): 98 99 def __init__(self, name, status): 100 self.name = name 101 self.status = status 102 103 def __cmp__(self, other): 104 if isinstance(other, AdbDevice): 105 return cmp(self.name, other.name) 106 else: 107 return cmp(self.name, other) 108 109 def __str__(self): 110 return 'AdbDevice({}, {})'.format(self.name, self.status) 111 112 __repr__ = __str__ 113 114 115class ApkInfo(object): 116 117 version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'") 118 name_regex = re.compile(r"name='(?P<name>[^']+)'") 119 120 def __init__(self, path=None): 121 self.path = path 122 self.package = None 123 self.activity = None 124 self.label = None 125 self.version_name = None 126 self.version_code = None 127 self.parse(path) 128 129 def parse(self, apk_path): 130 _check_env() 131 command = [aapt, 'dump', 'badging', apk_path] 132 logger.debug(' '.join(command)) 133 output = subprocess.check_output(command) 134 for line in output.split('\n'): 135 if line.startswith('application-label:'): 136 self.label = line.split(':')[1].strip().replace('\'', '') 137 elif line.startswith('package:'): 138 match = self.version_regex.search(line) 139 if match: 140 self.package = match.group('name') 141 self.version_code = match.group('vcode') 142 self.version_name = match.group('vname') 143 elif line.startswith('launchable-activity:'): 144 match = self.name_regex.search(line) 145 self.activity = match.group('name') 146 else: 147 pass # not interested 148 149 150class AdbConnection(object): 151 152 # maintains the count of parallel active connections to a device, so that 153 # adb disconnect is not invoked untill all connections are closed 154 active_connections = defaultdict(int) 155 default_timeout = 10 156 ls_command = 'ls' 157 158 @property 159 def name(self): 160 return self.device 161 162 @property 163 @memoized 164 def newline_separator(self): 165 output = adb_command(self.device, 166 "shell '({}); echo \"\n$?\"'".format(self.ls_command)) 167 if output.endswith('\r\n'): 168 return '\r\n' 169 elif output.endswith('\n'): 170 return '\n' 171 else: 172 raise DevlibError("Unknown line ending") 173 174 # Again, we need to handle boards where the default output format from ls is 175 # single column *and* boards where the default output is multi-column. 176 # We need to do this purely because the '-1' option causes errors on older 177 # versions of the ls tool in Android pre-v7. 178 def _setup_ls(self): 179 command = "shell '(ls -1); echo \"\n$?\"'" 180 try: 181 output = adb_command(self.device, command, timeout=self.timeout) 182 except subprocess.CalledProcessError as e: 183 raise HostError( 184 'Failed to set up ls command on Android device. Output:\n' 185 + e.output) 186 lines = output.splitlines() 187 retval = lines[-1].strip() 188 if int(retval) == 0: 189 self.ls_command = 'ls -1' 190 else: 191 self.ls_command = 'ls' 192 logger.debug("ls command is set to {}".format(self.ls_command)) 193 194 def __init__(self, device=None, timeout=None, platform=None): 195 self.timeout = timeout if timeout is not None else self.default_timeout 196 if device is None: 197 device = adb_get_device(timeout=timeout) 198 self.device = device 199 adb_connect(self.device) 200 AdbConnection.active_connections[self.device] += 1 201 self._setup_ls() 202 203 def push(self, source, dest, timeout=None): 204 if timeout is None: 205 timeout = self.timeout 206 command = "push '{}' '{}'".format(source, dest) 207 if not os.path.exists(source): 208 raise HostError('No such file "{}"'.format(source)) 209 return adb_command(self.device, command, timeout=timeout) 210 211 def pull(self, source, dest, timeout=None): 212 if timeout is None: 213 timeout = self.timeout 214 # Pull all files matching a wildcard expression 215 if os.path.isdir(dest) and \ 216 ('*' in source or '?' in source): 217 command = 'shell {} {}'.format(self.ls_command, source) 218 output = adb_command(self.device, command, timeout=timeout) 219 for line in output.splitlines(): 220 command = "pull '{}' '{}'".format(line.strip(), dest) 221 adb_command(self.device, command, timeout=timeout) 222 return 223 command = "pull '{}' '{}'".format(source, dest) 224 return adb_command(self.device, command, timeout=timeout) 225 226 def execute(self, command, timeout=None, check_exit_code=False, 227 as_root=False, strip_colors=True): 228 return adb_shell(self.device, command, timeout, check_exit_code, 229 as_root, self.newline_separator) 230 231 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False): 232 return adb_background_shell(self.device, command, stdout, stderr, as_root) 233 234 def close(self): 235 AdbConnection.active_connections[self.device] -= 1 236 if AdbConnection.active_connections[self.device] <= 0: 237 adb_disconnect(self.device) 238 del AdbConnection.active_connections[self.device] 239 240 def cancel_running_command(self): 241 # adbd multiplexes commands so that they don't interfer with each 242 # other, so there is no need to explicitly cancel a running command 243 # before the next one can be issued. 244 pass 245 246 247def fastboot_command(command, timeout=None, device=None): 248 _check_env() 249 target = '-s {}'.format(device) if device else '' 250 full_command = 'fastboot {} {}'.format(target, command) 251 logger.debug(full_command) 252 output, _ = check_output(full_command, timeout, shell=True) 253 return output 254 255 256def fastboot_flash_partition(partition, path_to_image): 257 command = 'flash {} {}'.format(partition, path_to_image) 258 fastboot_command(command) 259 260 261def adb_get_device(timeout=None): 262 """ 263 Returns the serial number of a connected android device. 264 265 If there are more than one device connected to the machine, or it could not 266 find any device connected, :class:`devlib.exceptions.HostError` is raised. 267 """ 268 # TODO this is a hacky way to issue a adb command to all listed devices 269 270 # The output of calling adb devices consists of a heading line then 271 # a list of the devices sperated by new line 272 # The last line is a blank new line. in otherwords, if there is a device found 273 # then the output length is 2 + (1 for each device) 274 start = time.time() 275 while True: 276 output = adb_command(None, "devices").splitlines() # pylint: disable=E1103 277 output_length = len(output) 278 if output_length == 3: 279 # output[1] is the 2nd line in the output which has the device name 280 # Splitting the line by '\t' gives a list of two indexes, which has 281 # device serial in 0 number and device type in 1. 282 return output[1].split('\t')[0] 283 elif output_length > 3: 284 message = '{} Android devices found; either explicitly specify ' +\ 285 'the device you want, or make sure only one is connected.' 286 raise HostError(message.format(output_length - 2)) 287 else: 288 if timeout < time.time() - start: 289 raise HostError('No device is connected and available') 290 time.sleep(1) 291 292 293def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS): 294 _check_env() 295 # Connect is required only for ADB-over-IP 296 if "." not in device: 297 logger.debug('Device connected via USB, connect not required') 298 return 299 tries = 0 300 output = None 301 while tries <= attempts: 302 tries += 1 303 if device: 304 command = 'adb connect {}'.format(device) 305 logger.debug(command) 306 output, _ = check_output(command, shell=True, timeout=timeout) 307 if _ping(device): 308 break 309 time.sleep(10) 310 else: # did not connect to the device 311 message = 'Could not connect to {}'.format(device or 'a device') 312 if output: 313 message += '; got: "{}"'.format(output) 314 raise HostError(message) 315 316 317def adb_disconnect(device): 318 _check_env() 319 if not device: 320 return 321 if ":" in device and device in adb_list_devices(): 322 command = "adb disconnect " + device 323 logger.debug(command) 324 retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True) 325 if retval: 326 raise TargetError('"{}" returned {}'.format(command, retval)) 327 328 329def _ping(device): 330 _check_env() 331 device_string = ' -s {}'.format(device) if device else '' 332 command = "adb{} shell \"ls / > /dev/null\"".format(device_string) 333 logger.debug(command) 334 result = subprocess.call(command, stderr=subprocess.PIPE, shell=True) 335 if not result: 336 return True 337 else: 338 return False 339 340 341def adb_shell(device, command, timeout=None, check_exit_code=False, 342 as_root=False, newline_separator='\r\n'): # NOQA 343 _check_env() 344 if as_root: 345 command = 'echo \'{}\' | su'.format(escape_single_quotes(command)) 346 device_part = ['-s', device] if device else [] 347 348 # On older combinations of ADB/Android versions, the adb host command always 349 # exits with 0 if it was able to run the command on the target, even if the 350 # command failed (https://code.google.com/p/android/issues/detail?id=3254). 351 # Homogenise this behaviour by running the command then echoing the exit 352 # code. 353 adb_shell_command = '({}); echo \"\n$?\"'.format(command) 354 actual_command = ['adb'] + device_part + ['shell', adb_shell_command] 355 logger.debug('adb {} shell {}'.format(' '.join(device_part), command)) 356 raw_output, error = check_output(actual_command, timeout, shell=False) 357 if raw_output: 358 try: 359 output, exit_code, _ = raw_output.rsplit(newline_separator, 2) 360 except ValueError: 361 exit_code, _ = raw_output.rsplit(newline_separator, 1) 362 output = '' 363 else: # raw_output is empty 364 exit_code = '969696' # just because 365 output = '' 366 367 if check_exit_code: 368 exit_code = exit_code.strip() 369 re_search = AM_START_ERROR.findall('{}\n{}'.format(output, error)) 370 if exit_code.isdigit(): 371 if int(exit_code): 372 message = ('Got exit code {}\nfrom target command: {}\n' 373 'STDOUT: {}\nSTDERR: {}') 374 raise TargetError(message.format(exit_code, command, output, error)) 375 elif re_search: 376 message = 'Could not start activity; got the following:\n{}' 377 raise TargetError(message.format(re_search[0])) 378 else: # not all digits 379 if re_search: 380 message = 'Could not start activity; got the following:\n{}' 381 raise TargetError(message.format(re_search[0])) 382 else: 383 message = 'adb has returned early; did not get an exit code. '\ 384 'Was kill-server invoked?' 385 raise TargetError(message) 386 387 return output 388 389 390def adb_background_shell(device, command, 391 stdout=subprocess.PIPE, 392 stderr=subprocess.PIPE, 393 as_root=False): 394 """Runs the sepcified command in a subprocess, returning the the Popen object.""" 395 _check_env() 396 if as_root: 397 command = 'echo \'{}\' | su'.format(escape_single_quotes(command)) 398 device_string = ' -s {}'.format(device) if device else '' 399 full_command = 'adb{} shell "{}"'.format(device_string, escape_double_quotes(command)) 400 logger.debug(full_command) 401 return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True) 402 403 404def adb_list_devices(): 405 output = adb_command(None, 'devices') 406 devices = [] 407 for line in output.splitlines(): 408 parts = [p.strip() for p in line.split()] 409 if len(parts) == 2: 410 devices.append(AdbDevice(*parts)) 411 return devices 412 413 414def adb_command(device, command, timeout=None): 415 _check_env() 416 device_string = ' -s {}'.format(device) if device else '' 417 full_command = "adb{} {}".format(device_string, command) 418 logger.debug(full_command) 419 output, _ = check_output(full_command, timeout, shell=True) 420 return output 421 422 423# Messy environment initialisation stuff... 424 425class _AndroidEnvironment(object): 426 427 def __init__(self): 428 self.android_home = None 429 self.platform_tools = None 430 self.adb = None 431 self.aapt = None 432 self.fastboot = None 433 434 435def _initialize_with_android_home(env): 436 logger.debug('Using ANDROID_HOME from the environment.') 437 env.android_home = android_home 438 env.platform_tools = os.path.join(android_home, 'platform-tools') 439 os.environ['PATH'] = env.platform_tools + os.pathsep + os.environ['PATH'] 440 _init_common(env) 441 return env 442 443 444def _initialize_without_android_home(env): 445 adb_full_path = which('adb') 446 if adb_full_path: 447 env.adb = 'adb' 448 else: 449 raise HostError('ANDROID_HOME is not set and adb is not in PATH. ' 450 'Have you installed Android SDK?') 451 logger.debug('Discovering ANDROID_HOME from adb path.') 452 env.platform_tools = os.path.dirname(adb_full_path) 453 env.android_home = os.path.dirname(env.platform_tools) 454 try: 455 _init_common(env) 456 except: 457 env.aapt = which('aapt') 458 if env.aapt: 459 logger.info('Using aapt: ' + env.aapt) 460 else: 461 raise RuntimeError('aapt not found, try setting ANDROID_HOME to \ 462 Android SDK or run LISA from android environment') 463 return env 464 465 466def _init_common(env): 467 logger.debug('ANDROID_HOME: {}'.format(env.android_home)) 468 build_tools_directory = os.path.join(env.android_home, 'build-tools') 469 if not os.path.isdir(build_tools_directory): 470 msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install 471 (cannot find build-tools)''' 472 raise HostError(msg.format(env.android_home)) 473 versions = os.listdir(build_tools_directory) 474 for version in reversed(sorted(versions)): 475 aapt_path = os.path.join(build_tools_directory, version, 'aapt') 476 if os.path.isfile(aapt_path): 477 logger.debug('Using aapt for version {}'.format(version)) 478 env.aapt = aapt_path 479 break 480 else: 481 raise HostError('aapt not found. Please make sure at least one Android ' 482 'platform is installed.') 483 484 485def _check_env(): 486 global android_home, platform_tools, adb, aapt # pylint: disable=W0603 487 if not android_home: 488 android_home = os.getenv('ANDROID_HOME') 489 if android_home: 490 _env = _initialize_with_android_home(_AndroidEnvironment()) 491 else: 492 _env = _initialize_without_android_home(_AndroidEnvironment()) 493 android_home = _env.android_home 494 platform_tools = _env.platform_tools 495 adb = _env.adb 496 aapt = _env.aapt 497