blob: 8334d6f2ebea34a0ab63506a12648a8051079d15 [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
29from devlib.exception import TargetError, HostError
30from devlib.utils.misc import check_output, which
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 class {[\w|.|/]*} does not exist")
38
39# See:
40# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
41ANDROID_VERSION_MAP = {
42 22: 'LOLLYPOP_MR1',
43 21: 'LOLLYPOP',
44 20: 'KITKAT_WATCH',
45 19: 'KITKAT',
46 18: 'JELLY_BEAN_MR2',
47 17: 'JELLY_BEAN_MR1',
48 16: 'JELLY_BEAN',
49 15: 'ICE_CREAM_SANDWICH_MR1',
50 14: 'ICE_CREAM_SANDWICH',
51 13: 'HONEYCOMB_MR2',
52 12: 'HONEYCOMB_MR1',
53 11: 'HONEYCOMB',
54 10: 'GINGERBREAD_MR1',
55 9: 'GINGERBREAD',
56 8: 'FROYO',
57 7: 'ECLAIR_MR1',
58 6: 'ECLAIR_0_1',
59 5: 'ECLAIR',
60 4: 'DONUT',
61 3: 'CUPCAKE',
62 2: 'BASE_1_1',
63 1: 'BASE',
64}
65
66
67# Initialized in functions near the botton of the file
68android_home = None
69platform_tools = None
70adb = None
71aapt = None
72fastboot = None
73
74
75class AndroidProperties(object):
76
77 def __init__(self, text):
78 self._properties = {}
79 self.parse(text)
80
81 def parse(self, text):
82 self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text))
83
84 def iteritems(self):
85 return self._properties.iteritems()
86
87 def __iter__(self):
88 return iter(self._properties)
89
90 def __getattr__(self, name):
91 return self._properties.get(name)
92
93 __getitem__ = __getattr__
94
95
96class AdbDevice(object):
97
98 def __init__(self, name, status):
99 self.name = name
100 self.status = status
101
102 def __cmp__(self, other):
103 if isinstance(other, AdbDevice):
104 return cmp(self.name, other.name)
105 else:
106 return cmp(self.name, other)
107
108 def __str__(self):
109 return 'AdbDevice({}, {})'.format(self.name, self.status)
110
111 __repr__ = __str__
112
113
114class ApkInfo(object):
115
116 version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'")
117 name_regex = re.compile(r"name='(?P<name>[^']+)'")
118
119 def __init__(self, path=None):
120 self.path = path
121 self.package = None
122 self.activity = None
123 self.label = None
124 self.version_name = None
125 self.version_code = None
126 self.parse(path)
127
128 def parse(self, apk_path):
129 _check_env()
130 command = [aapt, 'dump', 'badging', apk_path]
131 logger.debug(' '.join(command))
132 output = subprocess.check_output(command)
133 for line in output.split('\n'):
134 if line.startswith('application-label:'):
135 self.label = line.split(':')[1].strip().replace('\'', '')
136 elif line.startswith('package:'):
137 match = self.version_regex.search(line)
138 if match:
139 self.package = match.group('name')
140 self.version_code = match.group('vcode')
141 self.version_name = match.group('vname')
142 elif line.startswith('launchable-activity:'):
143 match = self.name_regex.search(line)
144 self.activity = match.group('name')
145 else:
146 pass # not interested
147
148
149class AdbConnection(object):
150
151 # maintains the count of parallel active connections to a device, so that
152 # adb disconnect is not invoked untill all connections are closed
153 active_connections = defaultdict(int)
154
155 @property
156 def name(self):
157 return self.device
158
159 def __init__(self, device=None, timeout=10):
160 self.timeout = timeout
161 if device is None:
162 device = adb_get_device(timeout=timeout)
163 self.device = device
164 adb_connect(self.device)
165 AdbConnection.active_connections[self.device] += 1
166
167 def push(self, source, dest, timeout=None):
168 if timeout is None:
169 timeout = self.timeout
170 command = 'push {} {}'.format(source, dest)
171 return adb_command(self.device, command, timeout=timeout)
172
173 def pull(self, source, dest, timeout=None):
174 if timeout is None:
175 timeout = self.timeout
176 command = 'pull {} {}'.format(source, dest)
177 return adb_command(self.device, command, timeout=timeout)
178
179 def execute(self, command, timeout=None, check_exit_code=False, as_root=False):
180 return adb_shell(self.device, command, timeout, check_exit_code, as_root)
181
182 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
183 return adb_background_shell(self.device, command, stdout, stderr, as_root)
184
185 def close(self):
186 AdbConnection.active_connections[self.device] -= 1
187 if AdbConnection.active_connections[self.device] <= 0:
188 adb_disconnect(self.device)
189 del AdbConnection.active_connections[self.device]
190
191 def cancel_running_command(self):
192 # adbd multiplexes commands so that they don't interfer with each
193 # other, so there is no need to explicitly cancel a running command
194 # before the next one can be issued.
195 pass
196
197
198def fastboot_command(command, timeout=None):
199 _check_env()
200 full_command = "fastboot {}".format(command)
201 logger.debug(full_command)
202 output, _ = check_output(full_command, timeout, shell=True)
203 return output
204
205
206def fastboot_flash_partition(partition, path_to_image):
207 command = 'flash {} {}'.format(partition, path_to_image)
208 fastboot_command(command)
209
210
211def adb_get_device(timeout=None):
212 """
213 Returns the serial number of a connected android device.
214
215 If there are more than one device connected to the machine, or it could not
216 find any device connected, :class:`devlib.exceptions.HostError` is raised.
217 """
218 # TODO this is a hacky way to issue a adb command to all listed devices
219
220 # The output of calling adb devices consists of a heading line then
221 # a list of the devices sperated by new line
222 # The last line is a blank new line. in otherwords, if there is a device found
223 # then the output length is 2 + (1 for each device)
224 start = time.time()
225 while True:
226 output = adb_command(None, "devices").splitlines() # pylint: disable=E1103
227 output_length = len(output)
228 if output_length == 3:
229 # output[1] is the 2nd line in the output which has the device name
230 # Splitting the line by '\t' gives a list of two indexes, which has
231 # device serial in 0 number and device type in 1.
232 return output[1].split('\t')[0]
233 elif output_length > 3:
234 message = '{} Android devices found; either explicitly specify ' +\
235 'the device you want, or make sure only one is connected.'
236 raise HostError(message.format(output_length - 2))
237 else:
238 if timeout < time.time() - start:
239 raise HostError('No device is connected and available')
240 time.sleep(1)
241
242
243def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
244 _check_env()
245 tries = 0
246 output = None
247 while tries <= attempts:
248 tries += 1
249 if device:
250 command = 'adb connect {}'.format(device)
251 logger.debug(command)
252 output, _ = check_output(command, shell=True, timeout=timeout)
253 if _ping(device):
254 break
255 time.sleep(10)
256 else: # did not connect to the device
257 message = 'Could not connect to {}'.format(device or 'a device')
258 if output:
259 message += '; got: "{}"'.format(output)
260 raise HostError(message)
261
262
263def adb_disconnect(device):
264 _check_env()
265 if not device:
266 return
267 if ":" in device:
268 command = "adb disconnect " + device
269 logger.debug(command)
270 retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
271 if retval:
272 raise TargetError('"{}" returned {}'.format(command, retval))
273
274
275def _ping(device):
276 _check_env()
277 device_string = ' -s {}'.format(device) if device else ''
278 command = "adb{} shell \"ls / > /dev/null\"".format(device_string)
279 logger.debug(command)
280 result = subprocess.call(command, stderr=subprocess.PIPE, shell=True)
281 if not result:
282 return True
283 else:
284 return False
285
286
287def adb_shell(device, command, timeout=None, check_exit_code=False, as_root=False): # NOQA
288 _check_env()
289 if as_root:
Sergei Trofimov171cc252015-12-14 17:21:47 +0000290 command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100291 device_string = ' -s {}'.format(device) if device else ''
292 full_command = 'adb{} shell "{}"'.format(device_string,
293 escape_double_quotes(command))
294 logger.debug(full_command)
295 if check_exit_code:
Sergei Trofimovf52bf792015-12-11 17:18:18 +0000296 actual_command = "adb{} shell '({}); echo \"\n$?\"'".format(device_string,
Sergei Trofimov64261a62015-11-24 12:50:02 +0000297 escape_single_quotes(command))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100298 raw_output, error = check_output(actual_command, timeout, shell=True)
299 if raw_output:
300 try:
Sergei Trofimov64261a62015-11-24 12:50:02 +0000301 output, exit_code, _ = raw_output.rsplit('\r\n', 2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100302 except ValueError:
Sergei Trofimov64261a62015-11-24 12:50:02 +0000303 exit_code, _ = raw_output.rsplit('\r\n', 1)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100304 output = ''
305 else: # raw_output is empty
306 exit_code = '969696' # just because
307 output = ''
308
309 exit_code = exit_code.strip()
310 if exit_code.isdigit():
311 if int(exit_code):
312 message = 'Got exit code {}\nfrom: {}\nSTDOUT: {}\nSTDERR: {}'
313 raise TargetError(message.format(exit_code, full_command, output, error))
314 elif AM_START_ERROR.findall(output):
315 message = 'Could not start activity; got the following:'
316 message += '\n{}'.format(AM_START_ERROR.findall(output)[0])
317 raise TargetError(message)
318 else: # not all digits
319 if AM_START_ERROR.findall(output):
320 message = 'Could not start activity; got the following:\n{}'
321 raise TargetError(message.format(AM_START_ERROR.findall(output)[0]))
322 else:
323 message = 'adb has returned early; did not get an exit code. '\
324 'Was kill-server invoked?'
325 raise TargetError(message)
326 else: # do not check exit code
327 output, _ = check_output(full_command, timeout, shell=True)
328 return output
329
330
331def adb_background_shell(device, command,
332 stdout=subprocess.PIPE,
333 stderr=subprocess.PIPE,
334 as_root=False):
335 """Runs the sepcified command in a subprocess, returning the the Popen object."""
336 _check_env()
337 if as_root:
338 command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
339 device_string = ' -s {}'.format(device) if device else ''
340 full_command = 'adb{} shell "{}"'.format(device_string, escape_double_quotes(command))
341 logger.debug(full_command)
342 return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
343
344
345def adb_list_devices():
346 output = adb_command(None, 'devices')
347 devices = []
348 for line in output.splitlines():
349 parts = [p.strip() for p in line.split()]
350 if len(parts) == 2:
351 devices.append(AdbDevice(*parts))
352 return devices
353
354
355def adb_command(device, command, timeout=None):
356 _check_env()
357 device_string = ' -s {}'.format(device) if device else ''
358 full_command = "adb{} {}".format(device_string, command)
359 logger.debug(full_command)
360 output, _ = check_output(full_command, timeout, shell=True)
361 return output
362
363
364# Messy environment initialisation stuff...
365
366class _AndroidEnvironment(object):
367
368 def __init__(self):
369 self.android_home = None
370 self.platform_tools = None
371 self.adb = None
372 self.aapt = None
373 self.fastboot = None
374
375
376def _initialize_with_android_home(env):
377 logger.debug('Using ANDROID_HOME from the environment.')
378 env.android_home = android_home
379 env.platform_tools = os.path.join(android_home, 'platform-tools')
380 os.environ['PATH'] += os.pathsep + env.platform_tools
381 _init_common(env)
382 return env
383
384
385def _initialize_without_android_home(env):
386 if which('adb'):
387 env.adb = 'adb'
388 else:
389 raise HostError('ANDROID_HOME is not set and adb is not in PATH. '
390 'Have you installed Android SDK?')
391 logger.debug('Discovering ANDROID_HOME from adb path.')
392 env.platform_tools = os.path.dirname(env.adb)
393 env.android_home = os.path.dirname(env.platform_tools)
394 _init_common(env)
395 return env
396
397
398def _init_common(env):
399 logger.debug('ANDROID_HOME: {}'.format(env.android_home))
400 build_tools_directory = os.path.join(env.android_home, 'build-tools')
401 if not os.path.isdir(build_tools_directory):
402 msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install
403 (cannot find build-tools)'''
404 raise HostError(msg.format(env.android_home))
405 versions = os.listdir(build_tools_directory)
406 for version in reversed(sorted(versions)):
407 aapt_path = os.path.join(build_tools_directory, version, 'aapt')
408 if os.path.isfile(aapt_path):
409 logger.debug('Using aapt for version {}'.format(version))
410 env.aapt = aapt_path
411 break
412 else:
413 raise HostError('aapt not found. Please make sure at least one Android '
414 'platform is installed.')
415
416
417def _check_env():
418 global android_home, platform_tools, adb, aapt # pylint: disable=W0603
419 if not android_home:
420 android_home = os.getenv('ANDROID_HOME')
421 if android_home:
422 _env = _initialize_with_android_home(_AndroidEnvironment())
423 else:
424 _env = _initialize_without_android_home(_AndroidEnvironment())
425 android_home = _env.android_home
426 platform_tools = _env.platform_tools
427 adb = _env.adb
428 aapt = _env.aapt