blob: e8ca78d4365da0ece1b1faa7944b1f1b8c43eeb8 [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001# 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
Sebastian Goscik8de24b52016-03-23 15:10:26 +000029from devlib.exception import TargetError, HostError, DevlibError
30from devlib.utils.misc import check_output, which, memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010031from devlib.utils.misc import escape_single_quotes, escape_double_quotes
32
33
34logger = logging.getLogger('android')
35
36MAX_ATTEMPTS = 5
Valentin Schneider2d968402017-06-12 18:59:44 +010037AM_START_ERROR = re.compile(r"Error: Activity.*")
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010038
39# See:
40# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
41ANDROID_VERSION_MAP = {
Sebastian Goscik0c112892016-02-15 15:35:56 +000042 23: 'MARSHMALLOW',
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010043 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)
Sergei Trofimov89256fd2016-05-17 14:00:01 +0100155 default_timeout = 10
Chris Redpathe8e945a2016-10-14 14:35:58 +0100156 ls_command = 'ls'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100157
158 @property
159 def name(self):
160 return self.device
161
Sebastian Goscik8de24b52016-03-23 15:10:26 +0000162 @property
163 @memoized
164 def newline_separator(self):
Chris Redpathe8e945a2016-10-14 14:35:58 +0100165 output = adb_command(self.device,
166 "shell '({}); echo \"\n$?\"'".format(self.ls_command))
Sebastian Goscik8de24b52016-03-23 15:10:26 +0000167 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
Chris Redpathe8e945a2016-10-14 14:35:58 +0100174 # 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$?\"'"
Brendan Jackman32982052017-03-01 18:49:27 +0000180 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)
Chris Redpathe8e945a2016-10-14 14:35:58 +0100186 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'
Sergei Trofimov97a89972017-05-12 10:45:42 +0100192 logger.debug("ls command is set to {}".format(self.ls_command))
Chris Redpathe8e945a2016-10-14 14:35:58 +0100193
Anouk Van Laer29a79402017-01-31 12:48:58 +0000194 def __init__(self, device=None, timeout=None, platform=None):
Sergei Trofimov89256fd2016-05-17 14:00:01 +0100195 self.timeout = timeout if timeout is not None else self.default_timeout
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100196 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
Chris Redpathe8e945a2016-10-14 14:35:58 +0100201 self._setup_ls()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100202
203 def push(self, source, dest, timeout=None):
204 if timeout is None:
205 timeout = self.timeout
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000206 command = "push '{}' '{}'".format(source, dest)
Brendan Jackmane17b9c32017-04-28 11:26:35 +0100207 if not os.path.exists(source):
208 raise HostError('No such file "{}"'.format(source))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100209 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
Patrick Bellasic93e3d62015-11-25 11:19:08 +0000214 # Pull all files matching a wildcard expression
215 if os.path.isdir(dest) and \
Sebastian Goscikaab487c2016-02-15 15:21:40 +0000216 ('*' in source or '?' in source):
Chris Redpathe8e945a2016-10-14 14:35:58 +0100217 command = 'shell {} {}'.format(self.ls_command, source)
Patrick Bellasic93e3d62015-11-25 11:19:08 +0000218 output = adb_command(self.device, command, timeout=timeout)
219 for line in output.splitlines():
Brendan Jackmanee38a422016-11-18 17:48:48 +0000220 command = "pull '{}' '{}'".format(line.strip(), dest)
Patrick Bellasic93e3d62015-11-25 11:19:08 +0000221 adb_command(self.device, command, timeout=timeout)
222 return
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000223 command = "pull '{}' '{}'".format(source, dest)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100224 return adb_command(self.device, command, timeout=timeout)
225
Anouk Van Laer29a79402017-01-31 12:48:58 +0000226 def execute(self, command, timeout=None, check_exit_code=False,
227 as_root=False, strip_colors=True):
Patrick Bellasic2329bd2016-03-28 12:29:45 +0100228 return adb_shell(self.device, command, timeout, check_exit_code,
229 as_root, self.newline_separator)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100230
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
Patrick Bellasida588ea2017-02-16 13:08:31 +0000247def fastboot_command(command, timeout=None, device=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100248 _check_env()
Patrick Bellasida588ea2017-02-16 13:08:31 +0000249 target = '-s {}'.format(device) if device else ''
250 full_command = 'fastboot {} {}'.format(target, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100251 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 ' +\
Sebastian Goscikaab487c2016-02-15 15:21:40 +0000285 'the device you want, or make sure only one is connected.'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100286 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()
Patrick Bellasif714dd32016-07-15 11:18:09 +0100295 # 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
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100299 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
Chris Redpath119fd7d2016-10-06 16:34:14 +0100321 if ":" in device and device in adb_list_devices():
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100322 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
Patrick Bellasic2329bd2016-03-28 12:29:45 +0100341def adb_shell(device, command, timeout=None, check_exit_code=False,
342 as_root=False, newline_separator='\r\n'): # NOQA
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100343 _check_env()
344 if as_root:
Sergei Trofimov171cc252015-12-14 17:21:47 +0000345 command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
Marc Bonnicib59f7c32016-11-01 17:17:04 +0000346 device_part = ['-s', device] if device else []
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100347
Brendan Jackman44fe0372017-01-25 12:06:38 +0000348 # 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:
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100368 exit_code = exit_code.strip()
Valentin Schneider2d968402017-06-12 18:59:44 +0100369 re_search = AM_START_ERROR.findall('{}\n{}'.format(output, error))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100370 if exit_code.isdigit():
371 if int(exit_code):
Brendan Jackman44fe0372017-01-25 12:06:38 +0000372 message = ('Got exit code {}\nfrom target command: {}\n'
373 'STDOUT: {}\nSTDERR: {}')
374 raise TargetError(message.format(exit_code, command, output, error))
Valentin Schneider2d968402017-06-12 18:59:44 +0100375 elif re_search:
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100376 message = 'Could not start activity; got the following:\n{}'
Valentin Schneider2d968402017-06-12 18:59:44 +0100377 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]))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100382 else:
383 message = 'adb has returned early; did not get an exit code. '\
Sergei Trofimov85036fb2017-07-12 13:44:47 +0100384 'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\
385 '-----\nERROR:\n-----\n{}\n-----'
386 raise TargetError(message.format(raw_output, error))
Brendan Jackman44fe0372017-01-25 12:06:38 +0000387
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100388 return output
389
390
391def adb_background_shell(device, command,
392 stdout=subprocess.PIPE,
393 stderr=subprocess.PIPE,
394 as_root=False):
395 """Runs the sepcified command in a subprocess, returning the the Popen object."""
396 _check_env()
397 if as_root:
398 command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
399 device_string = ' -s {}'.format(device) if device else ''
400 full_command = 'adb{} shell "{}"'.format(device_string, escape_double_quotes(command))
401 logger.debug(full_command)
402 return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
403
404
405def adb_list_devices():
406 output = adb_command(None, 'devices')
407 devices = []
408 for line in output.splitlines():
409 parts = [p.strip() for p in line.split()]
410 if len(parts) == 2:
411 devices.append(AdbDevice(*parts))
412 return devices
413
414
415def adb_command(device, command, timeout=None):
416 _check_env()
417 device_string = ' -s {}'.format(device) if device else ''
418 full_command = "adb{} {}".format(device_string, command)
419 logger.debug(full_command)
420 output, _ = check_output(full_command, timeout, shell=True)
421 return output
422
423
424# Messy environment initialisation stuff...
425
426class _AndroidEnvironment(object):
427
428 def __init__(self):
429 self.android_home = None
430 self.platform_tools = None
431 self.adb = None
432 self.aapt = None
433 self.fastboot = None
434
435
436def _initialize_with_android_home(env):
437 logger.debug('Using ANDROID_HOME from the environment.')
438 env.android_home = android_home
439 env.platform_tools = os.path.join(android_home, 'platform-tools')
Sergei Trofimovd5460e12016-12-13 11:26:32 +0000440 os.environ['PATH'] = env.platform_tools + os.pathsep + os.environ['PATH']
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100441 _init_common(env)
442 return env
443
444
445def _initialize_without_android_home(env):
Javi Merino7f32efc2015-12-15 13:43:56 +0000446 adb_full_path = which('adb')
447 if adb_full_path:
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100448 env.adb = 'adb'
449 else:
450 raise HostError('ANDROID_HOME is not set and adb is not in PATH. '
451 'Have you installed Android SDK?')
452 logger.debug('Discovering ANDROID_HOME from adb path.')
Javi Merino7f32efc2015-12-15 13:43:56 +0000453 env.platform_tools = os.path.dirname(adb_full_path)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100454 env.android_home = os.path.dirname(env.platform_tools)
455 _init_common(env)
456 return env
457
458
459def _init_common(env):
460 logger.debug('ANDROID_HOME: {}'.format(env.android_home))
461 build_tools_directory = os.path.join(env.android_home, 'build-tools')
462 if not os.path.isdir(build_tools_directory):
463 msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install
464 (cannot find build-tools)'''
465 raise HostError(msg.format(env.android_home))
466 versions = os.listdir(build_tools_directory)
467 for version in reversed(sorted(versions)):
468 aapt_path = os.path.join(build_tools_directory, version, 'aapt')
469 if os.path.isfile(aapt_path):
470 logger.debug('Using aapt for version {}'.format(version))
471 env.aapt = aapt_path
472 break
473 else:
474 raise HostError('aapt not found. Please make sure at least one Android '
475 'platform is installed.')
476
477
478def _check_env():
479 global android_home, platform_tools, adb, aapt # pylint: disable=W0603
480 if not android_home:
481 android_home = os.getenv('ANDROID_HOME')
482 if android_home:
483 _env = _initialize_with_android_home(_AndroidEnvironment())
484 else:
485 _env = _initialize_without_android_home(_AndroidEnvironment())
486 android_home = _env.android_home
487 platform_tools = _env.platform_tools
488 adb = _env.adb
489 aapt = _env.aapt