android.py revision fb5a260f4b4879503bd5385f96a97612b0e49d13
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
27import threading
28import tempfile
29import Queue
30from collections import defaultdict
31
32from devlib.exception import TargetError, HostError, DevlibError
33from devlib.utils.misc import check_output, which, memoized, ABI_MAP
34from devlib.utils.misc import escape_single_quotes, escape_double_quotes
35from devlib import host
36
37
38logger = logging.getLogger('android')
39
40MAX_ATTEMPTS = 5
41AM_START_ERROR = re.compile(r"Error: Activity.*")
42
43# See:
44# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
45ANDROID_VERSION_MAP = {
46    23: 'MARSHMALLOW',
47    22: 'LOLLYPOP_MR1',
48    21: 'LOLLYPOP',
49    20: 'KITKAT_WATCH',
50    19: 'KITKAT',
51    18: 'JELLY_BEAN_MR2',
52    17: 'JELLY_BEAN_MR1',
53    16: 'JELLY_BEAN',
54    15: 'ICE_CREAM_SANDWICH_MR1',
55    14: 'ICE_CREAM_SANDWICH',
56    13: 'HONEYCOMB_MR2',
57    12: 'HONEYCOMB_MR1',
58    11: 'HONEYCOMB',
59    10: 'GINGERBREAD_MR1',
60    9: 'GINGERBREAD',
61    8: 'FROYO',
62    7: 'ECLAIR_MR1',
63    6: 'ECLAIR_0_1',
64    5: 'ECLAIR',
65    4: 'DONUT',
66    3: 'CUPCAKE',
67    2: 'BASE_1_1',
68    1: 'BASE',
69}
70
71
72# Initialized in functions near the botton of the file
73android_home = None
74platform_tools = None
75adb = None
76aapt = None
77fastboot = None
78
79
80class AndroidProperties(object):
81
82    def __init__(self, text):
83        self._properties = {}
84        self.parse(text)
85
86    def parse(self, text):
87        self._properties = dict(re.findall(r'\[(.*?)\]:\s+\[(.*?)\]', text))
88
89    def iteritems(self):
90        return self._properties.iteritems()
91
92    def __iter__(self):
93        return iter(self._properties)
94
95    def __getattr__(self, name):
96        return self._properties.get(name)
97
98    __getitem__ = __getattr__
99
100
101class AdbDevice(object):
102
103    def __init__(self, name, status):
104        self.name = name
105        self.status = status
106
107    def __cmp__(self, other):
108        if isinstance(other, AdbDevice):
109            return cmp(self.name, other.name)
110        else:
111            return cmp(self.name, other)
112
113    def __str__(self):
114        return 'AdbDevice({}, {})'.format(self.name, self.status)
115
116    __repr__ = __str__
117
118
119class ApkInfo(object):
120
121    version_regex = re.compile(r"name='(?P<name>[^']+)' versionCode='(?P<vcode>[^']+)' versionName='(?P<vname>[^']+)'")
122    name_regex = re.compile(r"name='(?P<name>[^']+)'")
123
124    def __init__(self, path=None):
125        self.path = path
126        self.package = None
127        self.activity = None
128        self.label = None
129        self.version_name = None
130        self.version_code = None
131        self.native_code = None
132        self.parse(path)
133
134    def parse(self, apk_path):
135        _check_env()
136        command = [aapt, 'dump', 'badging', apk_path]
137        logger.debug(' '.join(command))
138        output = subprocess.check_output(command)
139        for line in output.split('\n'):
140            if line.startswith('application-label:'):
141                self.label = line.split(':')[1].strip().replace('\'', '')
142            elif line.startswith('package:'):
143                match = self.version_regex.search(line)
144                if match:
145                    self.package = match.group('name')
146                    self.version_code = match.group('vcode')
147                    self.version_name = match.group('vname')
148            elif line.startswith('launchable-activity:'):
149                match = self.name_regex.search(line)
150                self.activity = match.group('name')
151            elif line.startswith('native-code'):
152                apk_abis = [entry.strip() for entry in line.split(':')[1].split("'") if entry.strip()]
153                mapped_abis = []
154                for apk_abi in apk_abis:
155                    found = False
156                    for abi, architectures in ABI_MAP.iteritems():
157                        if apk_abi in architectures:
158                            mapped_abis.append(abi)
159                            found = True
160                            break
161                    if not found:
162                        mapped_abis.append(apk_abi)
163                self.native_code = mapped_abis
164            else:
165                pass  # not interested
166
167
168class AdbConnection(object):
169
170    # maintains the count of parallel active connections to a device, so that
171    # adb disconnect is not invoked untill all connections are closed
172    active_connections = defaultdict(int)
173    default_timeout = 10
174    ls_command = 'ls'
175
176    @property
177    def name(self):
178        return self.device
179
180    @property
181    @memoized
182    def newline_separator(self):
183        output = adb_command(self.device,
184                             "shell '({}); echo \"\n$?\"'".format(self.ls_command), adb_server=self.adb_server)
185        if output.endswith('\r\n'):
186            return '\r\n'
187        elif output.endswith('\n'):
188            return '\n'
189        else:
190            raise DevlibError("Unknown line ending")
191
192    # Again, we need to handle boards where the default output format from ls is
193    # single column *and* boards where the default output is multi-column.
194    # We need to do this purely because the '-1' option causes errors on older
195    # versions of the ls tool in Android pre-v7.
196    def _setup_ls(self):
197        command = "shell '(ls -1); echo \"\n$?\"'"
198        try:
199            output = adb_command(self.device, command, timeout=self.timeout, adb_server=self.adb_server)
200        except subprocess.CalledProcessError as e:
201            raise HostError(
202                'Failed to set up ls command on Android device. Output:\n'
203                + e.output)
204        lines = output.splitlines()
205        retval = lines[-1].strip()
206        if int(retval) == 0:
207            self.ls_command = 'ls -1'
208        else:
209            self.ls_command = 'ls'
210        logger.debug("ls command is set to {}".format(self.ls_command))
211
212    def __init__(self, device=None, timeout=None, platform=None, adb_server=None):
213        self.timeout = timeout if timeout is not None else self.default_timeout
214        if device is None:
215            device = adb_get_device(timeout=timeout, adb_server=adb_server)
216        self.device = device
217        self.adb_server = adb_server
218        adb_connect(self.device)
219        AdbConnection.active_connections[self.device] += 1
220        self._setup_ls()
221
222    def push(self, source, dest, timeout=None):
223        if timeout is None:
224            timeout = self.timeout
225        command = "push '{}' '{}'".format(source, dest)
226        if not os.path.exists(source):
227            raise HostError('No such file "{}"'.format(source))
228        return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
229
230    def pull(self, source, dest, timeout=None):
231        if timeout is None:
232            timeout = self.timeout
233        # Pull all files matching a wildcard expression
234        if os.path.isdir(dest) and \
235           ('*' in source or '?' in source):
236            command = 'shell {} {}'.format(self.ls_command, source)
237            output = adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
238            for line in output.splitlines():
239                command = "pull '{}' '{}'".format(line.strip(), dest)
240                adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
241            return
242        command = "pull '{}' '{}'".format(source, dest)
243        return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
244
245    def execute(self, command, timeout=None, check_exit_code=False,
246                as_root=False, strip_colors=True):
247        return adb_shell(self.device, command, timeout, check_exit_code,
248                         as_root, self.newline_separator,adb_server=self.adb_server)
249
250    def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
251        return adb_background_shell(self.device, command, stdout, stderr, as_root)
252
253    def close(self):
254        AdbConnection.active_connections[self.device] -= 1
255        if AdbConnection.active_connections[self.device] <= 0:
256            adb_disconnect(self.device)
257            del AdbConnection.active_connections[self.device]
258
259    def cancel_running_command(self):
260        # adbd multiplexes commands so that they don't interfer with each
261        # other, so there is no need to explicitly cancel a running command
262        # before the next one can be issued.
263        pass
264
265
266def fastboot_command(command, timeout=None, device=None):
267    _check_env()
268    target = '-s {}'.format(device) if device else ''
269    full_command = 'fastboot {} {}'.format(target, command)
270    logger.debug(full_command)
271    output, _ = check_output(full_command, timeout, shell=True)
272    return output
273
274
275def fastboot_flash_partition(partition, path_to_image):
276    command = 'flash {} {}'.format(partition, path_to_image)
277    fastboot_command(command)
278
279
280def adb_get_device(timeout=None, adb_server=None):
281    """
282    Returns the serial number of a connected android device.
283
284    If there are more than one device connected to the machine, or it could not
285    find any device connected, :class:`devlib.exceptions.HostError` is raised.
286    """
287    # TODO this is a hacky way to issue a adb command to all listed devices
288
289    # The output of calling adb devices consists of a heading line then
290    # a list of the devices sperated by new line
291    # The last line is a blank new line. in otherwords, if there is a device found
292    # then the output length is 2 + (1 for each device)
293    start = time.time()
294    while True:
295        output = adb_command(None, "devices", adb_server=adb_server).splitlines()  # pylint: disable=E1103
296        output_length = len(output)
297        if output_length == 3:
298            # output[1] is the 2nd line in the output which has the device name
299            # Splitting the line by '\t' gives a list of two indexes, which has
300            # device serial in 0 number and device type in 1.
301            return output[1].split('\t')[0]
302        elif output_length > 3:
303            message = '{} Android devices found; either explicitly specify ' +\
304                      'the device you want, or make sure only one is connected.'
305            raise HostError(message.format(output_length - 2))
306        else:
307            if timeout < time.time() - start:
308                raise HostError('No device is connected and available')
309            time.sleep(1)
310
311
312def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
313    _check_env()
314    # Connect is required only for ADB-over-IP
315    if "." not in device:
316        logger.debug('Device connected via USB, connect not required')
317        return
318    tries = 0
319    output = None
320    while tries <= attempts:
321        tries += 1
322        if device:
323            command = 'adb connect {}'.format(device)
324            logger.debug(command)
325            output, _ = check_output(command, shell=True, timeout=timeout)
326        if _ping(device):
327            break
328        time.sleep(10)
329    else:  # did not connect to the device
330        message = 'Could not connect to {}'.format(device or 'a device')
331        if output:
332            message += '; got: "{}"'.format(output)
333        raise HostError(message)
334
335
336def adb_disconnect(device):
337    _check_env()
338    if not device:
339        return
340    if ":" in device and device in adb_list_devices():
341        command = "adb disconnect " + device
342        logger.debug(command)
343        retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
344        if retval:
345            raise TargetError('"{}" returned {}'.format(command, retval))
346
347
348def _ping(device):
349    _check_env()
350    device_string = ' -s {}'.format(device) if device else ''
351    command = "adb{} shell \"ls / > /dev/null\"".format(device_string)
352    logger.debug(command)
353    result = subprocess.call(command, stderr=subprocess.PIPE, shell=True)
354    if not result:
355        return True
356    else:
357        return False
358
359
360def adb_shell(device, command, timeout=None, check_exit_code=False,
361              as_root=False, newline_separator='\r\n', adb_server=None):  # NOQA
362    _check_env()
363    if as_root:
364        command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
365    device_part = []
366    if adb_server:
367        device_part = ['-H', adb_server]
368    device_part += ['-s', device] if device else []
369
370    # On older combinations of ADB/Android versions, the adb host command always
371    # exits with 0 if it was able to run the command on the target, even if the
372    # command failed (https://code.google.com/p/android/issues/detail?id=3254).
373    # Homogenise this behaviour by running the command then echoing the exit
374    # code.
375    adb_shell_command = '({}); echo \"\n$?\"'.format(command)
376    actual_command = ['adb'] + device_part + ['shell', adb_shell_command]
377    logger.debug('adb {} shell {}'.format(' '.join(device_part), command))
378    raw_output, error = check_output(actual_command, timeout, shell=False)
379    if raw_output:
380        try:
381            output, exit_code, _ = raw_output.rsplit(newline_separator, 2)
382        except ValueError:
383            exit_code, _ = raw_output.rsplit(newline_separator, 1)
384            output = ''
385    else:  # raw_output is empty
386        exit_code = '969696'  # just because
387        output = ''
388
389    if check_exit_code:
390        exit_code = exit_code.strip()
391        re_search = AM_START_ERROR.findall('{}\n{}'.format(output, error))
392        if exit_code.isdigit():
393            if int(exit_code):
394                message = ('Got exit code {}\nfrom target command: {}\n'
395                           'STDOUT: {}\nSTDERR: {}')
396                raise TargetError(message.format(exit_code, command, output, error))
397            elif re_search:
398                message = 'Could not start activity; got the following:\n{}'
399                raise TargetError(message.format(re_search[0]))
400        else:  # not all digits
401            if re_search:
402                message = 'Could not start activity; got the following:\n{}'
403                raise TargetError(message.format(re_search[0]))
404            else:
405                message = 'adb has returned early; did not get an exit code. '\
406                          'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\
407                          '-----\nERROR:\n-----\n{}\n-----'
408                raise TargetError(message.format(raw_output, error))
409
410    return output
411
412
413def adb_background_shell(device, command,
414                         stdout=subprocess.PIPE,
415                         stderr=subprocess.PIPE,
416                         as_root=False):
417    """Runs the sepcified command in a subprocess, returning the the Popen object."""
418    _check_env()
419    if as_root:
420        command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
421    device_string = ' -s {}'.format(device) if device else ''
422    full_command = 'adb{} shell "{}"'.format(device_string, escape_double_quotes(command))
423    logger.debug(full_command)
424    return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
425
426
427def adb_list_devices(adb_server=None):
428    output = adb_command(None, 'devices',adb_server=adb_server)
429    devices = []
430    for line in output.splitlines():
431        parts = [p.strip() for p in line.split()]
432        if len(parts) == 2:
433            devices.append(AdbDevice(*parts))
434    return devices
435
436
437def adb_command(device, command, timeout=None,adb_server=None):
438    _check_env()
439    device_string = ""
440    if adb_server != None:
441        device_string = ' -H {}'.format(adb_server)
442    device_string += ' -s {}'.format(device) if device else ''
443    full_command = "adb{} {}".format(device_string, command)
444    logger.debug(full_command)
445    output, _ = check_output(full_command, timeout, shell=True)
446    return output
447
448def grant_app_permissions(target, package):
449    """
450    Grant an app all the permissions it may ask for
451    """
452    dumpsys = target.execute('dumpsys package {}'.format(package))
453
454    permissions = re.search(
455        'requested permissions:\s*(?P<permissions>(android.permission.+\s*)+)', dumpsys
456    )
457    if permissions is None:
458        return
459    permissions = permissions.group('permissions').replace(" ", "").splitlines()
460
461    for permission in permissions:
462        try:
463            target.execute('pm grant {} {}'.format(package, permission))
464        except TargetError:
465            logger.debug('Cannot grant {}'.format(permission))
466
467
468# Messy environment initialisation stuff...
469
470class _AndroidEnvironment(object):
471
472    def __init__(self):
473        self.android_home = None
474        self.platform_tools = None
475        self.adb = None
476        self.aapt = None
477        self.fastboot = None
478
479
480def _initialize_with_android_home(env):
481    logger.debug('Using ANDROID_HOME from the environment.')
482    env.android_home = android_home
483    env.platform_tools = os.path.join(android_home, 'platform-tools')
484    os.environ['PATH'] = env.platform_tools + os.pathsep + os.environ['PATH']
485    _init_common(env)
486    return env
487
488
489def _initialize_without_android_home(env):
490    adb_full_path = which('adb')
491    if adb_full_path:
492        env.adb = 'adb'
493    else:
494        raise HostError('ANDROID_HOME is not set and adb is not in PATH. '
495                        'Have you installed Android SDK?')
496    logger.debug('Discovering ANDROID_HOME from adb path.')
497    env.platform_tools = os.path.dirname(adb_full_path)
498    env.android_home = os.path.dirname(env.platform_tools)
499    _init_common(env)
500    return env
501
502
503def _init_common(env):
504    logger.debug('ANDROID_HOME: {}'.format(env.android_home))
505    build_tools_directory = os.path.join(env.android_home, 'build-tools')
506    if not os.path.isdir(build_tools_directory):
507        msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install
508                 (cannot find build-tools)'''
509        raise HostError(msg.format(env.android_home))
510    versions = os.listdir(build_tools_directory)
511    for version in reversed(sorted(versions)):
512        aapt_path = os.path.join(build_tools_directory, version, 'aapt')
513        if os.path.isfile(aapt_path):
514            logger.debug('Using aapt for version {}'.format(version))
515            env.aapt = aapt_path
516            break
517    else:
518        raise HostError('aapt not found. Please make sure at least one Android '
519                        'platform is installed.')
520
521
522def _check_env():
523    global android_home, platform_tools, adb, aapt  # pylint: disable=W0603
524    if not android_home:
525        android_home = os.getenv('ANDROID_HOME')
526        if android_home:
527            _env = _initialize_with_android_home(_AndroidEnvironment())
528        else:
529            _env = _initialize_without_android_home(_AndroidEnvironment())
530        android_home = _env.android_home
531        platform_tools = _env.platform_tools
532        adb = _env.adb
533        aapt = _env.aapt
534
535class LogcatMonitor(threading.Thread):
536    """
537    Helper class for monitoring Anroid's logcat
538
539    :param target: Android target to monitor
540    :type target: :class:`AndroidTarget`
541
542                    device. Logcat entries that don't match any will not be
543                    seen. If omitted, all entries will be sent to host.
544    :type regexps: list(str)
545    """
546
547    FLUSH_SIZE = 1000
548
549    @property
550    def logfile(self):
551        return self._logfile
552
553    def __init__(self, target, regexps=None):
554        super(LogcatMonitor, self).__init__()
555
556        self.target = target
557
558        self._stopped = threading.Event()
559        self._match_found = threading.Event()
560
561        self._sought = None
562        self._found = None
563
564        self._lines = Queue.Queue()
565        self._datalock = threading.Lock()
566        self._regexps = regexps
567
568    def start(self, outfile=None):
569        """
570        Start logcat and begin monitoring
571
572        :param outfile: Optional path to file to store all logcat entries
573        :type outfile: str
574        """
575        if outfile:
576            self._logfile = outfile
577        else:
578            fd, self._logfile = tempfile.mkstemp()
579            os.close(fd)
580        logger.debug('Logging to {}'.format(self._logfile))
581
582        super(LogcatMonitor, self).start()
583
584    def run(self):
585        self.target.clear_logcat()
586
587        logcat_cmd = 'logcat'
588
589        # Join all requested regexps with an 'or'
590        if self._regexps:
591            regexp = '{}'.format('|'.join(self._regexps))
592            if len(self._regexps) > 1:
593                regexp = '({})'.format(regexp)
594            logcat_cmd = '{} -e "{}"'.format(logcat_cmd, regexp)
595
596        logger.debug('logcat command ="{}"'.format(logcat_cmd))
597        self._logcat = self.target.background(logcat_cmd)
598
599        while not self._stopped.is_set():
600            line = self._logcat.stdout.readline(1024)
601            if line:
602                self._add_line(line)
603
604    def stop(self):
605        # Kill the underlying logcat process
606        # This will unblock self._logcat.stdout.readline()
607        host.kill_children(self._logcat.pid)
608        self._logcat.kill()
609
610        self._stopped.set()
611        self.join()
612
613        self._flush_lines()
614
615    def _add_line(self, line):
616        self._lines.put(line)
617
618        if self._sought and re.match(self._sought, line):
619            self._found = line
620            self._match_found.set()
621
622        if self._lines.qsize() >= self.FLUSH_SIZE:
623            self._flush_lines()
624
625    def _flush_lines(self):
626        with self._datalock:
627            with open(self._logfile, 'a') as fh:
628                while not self._lines.empty():
629                    fh.write(self._lines.get())
630
631    def clear_log(self):
632        with self._datalock:
633            while not self._lines.empty():
634                self._lines.get()
635
636            with open(self._logfile, 'w') as fh:
637                pass
638
639    def get_log(self):
640        """
641        Return the list of lines found by the monitor
642        """
643        self._flush_lines()
644
645        with self._datalock:
646            with open(self._logfile, 'r') as fh:
647                res = [line for line in fh]
648
649        return res
650
651    def search(self, regexp):
652        """
653        Search a line that matches a regexp in the logcat log
654        Return immediatly
655        """
656        res = []
657
658        self._flush_lines()
659
660        with self._datalock:
661            with open(self._logfile, 'r') as fh:
662                for line in fh:
663                    if re.match(regexp, line):
664                        res.append(line)
665
666        return res
667
668    def wait_for(self, regexp, timeout=30):
669        """
670        Search a line that matches a regexp in the logcat log
671        Wait for it to appear if it's not found
672
673        :param regexp: regexp to search
674        :type regexp: str
675
676        :param timeout: Timeout in seconds, before rasing RuntimeError.
677                        ``None`` means wait indefinitely
678        :type timeout: number
679
680        :returns: List of matched strings
681        """
682        res = self.search(regexp)
683
684        # Found some matches, return them
685        # Also return if thread not running
686        if len(res) > 0 or not self.is_alive():
687            return res
688
689        # Did not find any match, wait for one to pop up
690        self._sought = regexp
691        found = self._match_found.wait(timeout)
692        self._match_found.clear()
693        self._sought = None
694
695        if found:
696            return [self._found]
697        else:
698            raise RuntimeError('Logcat monitor timeout ({}s)'.format(timeout))
699