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