android.py revision 85036fbb303681cc09bbf7800aac1a9e4648a247
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?\nOUTPUT:\n-----\n{}\n'\
385                          '-----\nERROR:\n-----\n{}\n-----'
386                raise TargetError(message.format(raw_output, error))
387
388    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')
440    os.environ['PATH'] = env.platform_tools + os.pathsep + os.environ['PATH']
441    _init_common(env)
442    return env
443
444
445def _initialize_without_android_home(env):
446    adb_full_path = which('adb')
447    if adb_full_path:
448        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.')
453    env.platform_tools = os.path.dirname(adb_full_path)
454    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
490