• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3.4
2#
3#   Copyright 2016 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17from builtins import str
18from builtins import open
19
20import os
21import time
22import traceback
23
24from acts import logger as acts_logger
25from acts import signals
26from acts import utils
27from acts.controllers import adb
28from acts.controllers import android
29from acts.controllers import event_dispatcher
30from acts.controllers import fastboot
31
32ACTS_CONTROLLER_CONFIG_NAME = "AndroidDevice"
33ACTS_CONTROLLER_REFERENCE_NAME = "android_devices"
34
35ANDROID_DEVICE_PICK_ALL_TOKEN = "*"
36# Key name for adb logcat extra params in config file.
37ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = "adb_logcat_param"
38ANDROID_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
39ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
40
41class AndroidDeviceError(signals.ControllerError):
42    pass
43
44class DoesNotExistError(AndroidDeviceError):
45    """Raised when something that does not exist is referenced.
46    """
47
48def create(configs, logger):
49    if not configs:
50        raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
51    elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
52        ads = get_all_instances(logger=logger)
53    elif not isinstance(configs, list):
54        raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
55    elif isinstance(configs[0], str):
56        # Configs is a list of serials.
57        ads = get_instances(configs, logger)
58    else:
59        # Configs is a list of dicts.
60        ads = get_instances_with_configs(configs, logger)
61    connected_ads = list_adb_devices()
62    for ad in ads:
63        if ad.serial not in connected_ads:
64            raise DoesNotExistError(("Android device %s is specified in config"
65                                     " but is not attached.") % ad.serial)
66        ad.start_adb_logcat()
67        try:
68            ad.get_droid()
69            ad.ed.start()
70        except:
71            # This exception is logged here to help with debugging under py2,
72            # because "exception raised while processing another exception" is
73            # only printed under py3.
74            msg = "Failed to start sl4a on %s" % ad.serial
75            logger.exception(msg)
76            raise AndroidDeviceError(msg)
77    return ads
78
79def destroy(ads):
80    for ad in ads:
81        try:
82            ad.terminate_all_sessions()
83        except:
84            pass
85        if ad.adb_logcat_process:
86            ad.stop_adb_logcat()
87
88def _parse_device_list(device_list_str, key):
89    """Parses a byte string representing a list of devices. The string is
90    generated by calling either adb or fastboot.
91
92    Args:
93        device_list_str: Output of adb or fastboot.
94        key: The token that signifies a device in device_list_str.
95
96    Returns:
97        A list of android device serial numbers.
98    """
99    clean_lines = str(device_list_str, 'utf-8').strip().split('\n')
100    results = []
101    for line in clean_lines:
102        tokens = line.strip().split('\t')
103        if len(tokens) == 2 and tokens[1] == key:
104            results.append(tokens[0])
105    return results
106
107def list_adb_devices():
108    """List all android devices connected to the computer that are detected by
109    adb.
110
111    Returns:
112        A list of android device serials. Empty if there's none.
113    """
114    out = adb.AdbProxy().devices()
115    return _parse_device_list(out, "device")
116
117def list_fastboot_devices():
118    """List all android devices connected to the computer that are in in
119    fastboot mode. These are detected by fastboot.
120
121    Returns:
122        A list of android device serials. Empty if there's none.
123    """
124    out = fastboot.FastbootProxy().devices()
125    return _parse_device_list(out, "fastboot")
126
127def get_instances(serials, logger=None):
128    """Create AndroidDevice instances from a list of serials.
129
130    Args:
131        serials: A list of android device serials.
132        logger: A logger to be passed to each instance.
133
134    Returns:
135        A list of AndroidDevice objects.
136    """
137    results = []
138    for s in serials:
139        results.append(AndroidDevice(s, logger=logger))
140    return results
141
142def get_instances_with_configs(configs, logger=None):
143    """Create AndroidDevice instances from a list of json configs.
144
145    Each config should have the required key-value pair "serial".
146
147    Args:
148        configs: A list of dicts each representing the configuration of one
149            android device.
150        logger: A logger to be passed to each instance.
151
152    Returns:
153        A list of AndroidDevice objects.
154    """
155    results = []
156    for c in configs:
157        try:
158            serial = c.pop("serial")
159        except KeyError:
160            raise AndroidDeviceError(('Required value "serial" is missing in '
161                'AndroidDevice config %s.') % c)
162        ad = AndroidDevice(serial, logger=logger)
163        ad.load_config(c)
164        results.append(ad)
165    return results
166
167def get_all_instances(include_fastboot=False, logger=None):
168    """Create AndroidDevice instances for all attached android devices.
169
170    Args:
171        include_fastboot: Whether to include devices in bootloader mode or not.
172        logger: A logger to be passed to each instance.
173
174    Returns:
175        A list of AndroidDevice objects each representing an android device
176        attached to the computer.
177    """
178    if include_fastboot:
179        serial_list = list_adb_devices() + list_fastboot_devices()
180        return get_instances(serial_list, logger=logger)
181    return get_instances(list_adb_devices(), logger=logger)
182
183def filter_devices(ads, func):
184    """Finds the AndroidDevice instances from a list that match certain
185    conditions.
186
187    Args:
188        ads: A list of AndroidDevice instances.
189        func: A function that takes an AndroidDevice object and returns True
190            if the device satisfies the filter condition.
191
192    Returns:
193        A list of AndroidDevice instances that satisfy the filter condition.
194    """
195    results = []
196    for ad in ads:
197        if func(ad):
198            results.append(ad)
199    return results
200
201def get_device(ads, **kwargs):
202    """Finds a unique AndroidDevice instance from a list that has specific
203    attributes of certain values.
204
205    Example:
206        get_device(android_devices, label="foo", phone_number="1234567890")
207        get_device(android_devices, model="angler")
208
209    Args:
210        ads: A list of AndroidDevice instances.
211        kwargs: keyword arguments used to filter AndroidDevice instances.
212
213    Returns:
214        The target AndroidDevice instance.
215
216    Raises:
217        AndroidDeviceError is raised if none or more than one device is
218        matched.
219    """
220    def _get_device_filter(ad):
221        for k, v in kwargs.items():
222            if not hasattr(ad, k):
223                return False
224            elif getattr(ad, k) != v:
225                return False
226        return True
227    filtered = filter_devices(ads, _get_device_filter)
228    if not filtered:
229        raise AndroidDeviceError(("Could not find a target device that matches"
230                                  " condition: %s.") % kwargs)
231    elif len(filtered) == 1:
232        return filtered[0]
233    else:
234        serials = [ad.serial for ad in filtered]
235        raise AndroidDeviceError("More than one device matched: %s" % serials)
236
237def take_bug_reports(ads, test_name, begin_time):
238    """Takes bug reports on a list of android devices.
239
240    If you want to take a bug report, call this function with a list of
241    android_device objects in on_fail. But reports will be taken on all the
242    devices in the list concurrently. Bug report takes a relative long
243    time to take, so use this cautiously.
244
245    Args:
246        ads: A list of AndroidDevice instances.
247        test_name: Name of the test case that triggered this bug report.
248        begin_time: Logline format timestamp taken when the test started.
249    """
250    begin_time = acts_logger.normalize_log_line_timestamp(begin_time)
251    def take_br(test_name, begin_time, ad):
252        ad.take_bug_report(test_name, begin_time)
253    args = [(test_name, begin_time, ad) for ad in ads]
254    utils.concurrent_exec(take_br, args)
255
256class AndroidDevice:
257    """Class representing an android device.
258
259    Each object of this class represents one Android device in ACTS, including
260    handles to adb, fastboot, and sl4a clients. In addition to direct adb
261    commands, this object also uses adb port forwarding to talk to the Android
262    device.
263
264    Attributes:
265        serial: A string that's the serial number of the Androi device.
266        h_port: An integer that's the port number for adb port forwarding used
267                on the computer the Android device is connected
268        d_port: An integer  that's the port number used on the Android device
269                for adb port forwarding.
270        log: A LoggerProxy object used for the class's internal logging.
271        log_path: A string that is the path where all logs collected on this
272                  android device should be stored.
273        adb_logcat_process: A process that collects the adb logcat.
274        adb_logcat_file_path: A string that's the full path to the adb logcat
275                              file collected, if any.
276        adb: An AdbProxy object used for interacting with the device via adb.
277        fastboot: A FastbootProxy object used for interacting with the device
278                  via fastboot.
279    """
280
281    def __init__(self, serial="", host_port=None, device_port=8080,
282                 logger=None):
283        self.serial = serial
284        self.h_port = host_port
285        self.d_port = device_port
286        self.log = acts_logger.LoggerProxy(logger)
287        lp = self.log.log_path
288        self.log_path = os.path.join(lp, "AndroidDevice%s" % serial)
289        self._droid_sessions = {}
290        self._event_dispatchers = {}
291        self.adb_logcat_process = None
292        self.adb_logcat_file_path = None
293        self.adb = adb.AdbProxy(serial)
294        self.fastboot = fastboot.FastbootProxy(serial)
295        if not self.is_bootloader:
296            self.root_adb()
297
298    def __del__(self):
299        if self.h_port:
300            self.adb.forward("--remove tcp:%d" % self.h_port)
301        if self.adb_logcat_process:
302            self.stop_adb_logcat()
303
304    @property
305    def is_bootloader(self):
306        """True if the device is in bootloader mode.
307        """
308        return self.serial in list_fastboot_devices()
309
310    @property
311    def is_adb_root(self):
312        """True if adb is running as root for this device.
313        """
314        return "root" in self.adb.shell("id -u").decode("utf-8")
315
316    @property
317    def model(self):
318        """The Android code name for the device.
319        """
320        # If device is in bootloader mode, get mode name from fastboot.
321        if self.is_bootloader:
322            out = self.fastboot.getvar("product").strip()
323            # "out" is never empty because of the "total time" message fastboot
324            # writes to stderr.
325            lines = out.decode("utf-8").split('\n', 1)
326            if lines:
327                tokens = lines[0].split(' ')
328                if len(tokens) > 1:
329                    return tokens[1].lower()
330            return None
331        out = self.adb.shell('getprop | grep ro.build.product')
332        model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
333        if model == "sprout":
334            return model
335        else:
336            out = self.adb.shell('getprop | grep ro.product.name')
337            model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
338            return model
339
340    @property
341    def droid(self):
342        """The first sl4a session initiated on this device. None if there isn't
343        one.
344        """
345        try:
346            session_id = sorted(self._droid_sessions)[0]
347            return self._droid_sessions[session_id][0]
348        except IndexError:
349            return None
350
351    @property
352    def ed(self):
353        """The first event_dispatcher instance created on this device. None if
354        there isn't one.
355        """
356        try:
357            session_id = sorted(self._event_dispatchers)[0]
358            return self._event_dispatchers[session_id]
359        except IndexError:
360            return None
361
362    @property
363    def droids(self):
364        """A list of the active sl4a sessions on this device.
365
366        If multiple connections exist for the same session, only one connection
367        is listed.
368        """
369        keys = sorted(self._droid_sessions)
370        results = []
371        for k in keys:
372            results.append(self._droid_sessions[k][0])
373        return results
374
375    @property
376    def eds(self):
377        """A list of the event_dispatcher objects on this device.
378
379        The indexing of the list matches that of the droids property.
380        """
381        keys = sorted(self._event_dispatchers)
382        results = []
383        for k in keys:
384            results.append(self._event_dispatchers[k])
385        return results
386
387    @property
388    def is_adb_logcat_on(self):
389        """Whether there is an ongoing adb logcat collection.
390        """
391        if self.adb_logcat_process:
392            return True
393        return False
394
395    def load_config(self, config):
396        """Add attributes to the AndroidDevice object based on json config.
397
398        Args:
399            config: A dictionary representing the configs.
400
401        Raises:
402            AndroidDeviceError is raised if the config is trying to overwrite
403            an existing attribute.
404        """
405        for k, v in config.items():
406            if hasattr(self, k):
407                raise AndroidDeviceError(("Attempting to set existing "
408                    "attribute %s on %s") % (k, self.serial))
409            setattr(self, k, v)
410
411    def root_adb(self):
412        """Change adb to root mode for this device.
413        """
414        if not self.is_adb_root:
415            self.adb.root()
416            self.adb.wait_for_device()
417            self.adb.remount()
418            self.adb.wait_for_device()
419
420    def get_droid(self, handle_event=True):
421        """Create an sl4a connection to the device.
422
423        Return the connection handler 'droid'. By default, another connection
424        on the same session is made for EventDispatcher, and the dispatcher is
425        returned to the caller as well.
426        If sl4a server is not started on the device, try to start it.
427
428        Args:
429            handle_event: True if this droid session will need to handle
430                events.
431
432        Returns:
433            droid: Android object used to communicate with sl4a on the android
434                device.
435            ed: An optional EventDispatcher to organize events for this droid.
436
437        Examples:
438            Don't need event handling:
439            >>> ad = AndroidDevice()
440            >>> droid = ad.get_droid(False)
441
442            Need event handling:
443            >>> ad = AndroidDevice()
444            >>> droid, ed = ad.get_droid()
445        """
446        if not self.h_port or not adb.is_port_available(self.h_port):
447            self.h_port = adb.get_available_host_port()
448        self.adb.tcp_forward(self.h_port, self.d_port)
449        try:
450            droid = self.start_new_session()
451        except:
452            self.adb.start_sl4a()
453            droid = self.start_new_session()
454        if handle_event:
455            ed = self.get_dispatcher(droid)
456            return droid, ed
457        return droid
458
459    def get_dispatcher(self, droid):
460        """Return an EventDispatcher for an sl4a session
461
462        Args:
463            droid: Session to create EventDispatcher for.
464
465        Returns:
466            ed: An EventDispatcher for specified session.
467        """
468        ed_key = self.serial + str(droid.uid)
469        if ed_key in self._event_dispatchers:
470            if self._event_dispatchers[ed_key] is None:
471                raise AndroidDeviceError("EventDispatcher Key Empty")
472            self.log.debug("Returning existing key %s for event dispatcher!",
473                           ed_key)
474            return self._event_dispatchers[ed_key]
475        event_droid = self.add_new_connection_to_session(droid.uid)
476        ed = event_dispatcher.EventDispatcher(event_droid)
477        self._event_dispatchers[ed_key] = ed
478        return ed
479
480    def _is_timestamp_in_range(self, target, begin_time, end_time):
481        low = acts_logger.logline_timestamp_comparator(begin_time, target) <= 0
482        high = acts_logger.logline_timestamp_comparator(end_time, target) >= 0
483        return low and high
484
485    def cat_adb_log(self, tag, begin_time):
486        """Takes an excerpt of the adb logcat log from a certain time point to
487        current time.
488
489        Args:
490            tag: An identifier of the time period, usualy the name of a test.
491            begin_time: Logline format timestamp of the beginning of the time
492                period.
493        """
494        if not self.adb_logcat_file_path:
495            raise AndroidDeviceError(("Attempting to cat adb log when none has"
496                                      " been collected on Android device %s."
497                                      ) % self.serial)
498        end_time = acts_logger.get_log_line_timestamp()
499        self.log.debug("Extracting adb log from logcat.")
500        adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts")
501        utils.create_dir(adb_excerpt_path)
502        f_name = os.path.basename(self.adb_logcat_file_path)
503        out_name = f_name.replace("adblog,", "").replace(".txt", "")
504        out_name = ",{},{}.txt".format(begin_time, out_name)
505        tag_len = utils.MAX_FILENAME_LEN - len(out_name)
506        tag = tag[:tag_len]
507        out_name = tag + out_name
508        full_adblog_path = os.path.join(adb_excerpt_path, out_name)
509        with open(full_adblog_path, 'w', encoding='utf-8') as out:
510            in_file = self.adb_logcat_file_path
511            with open(in_file, 'r', encoding='utf-8', errors='replace') as f:
512                in_range = False
513                while True:
514                    line = None
515                    try:
516                        line = f.readline()
517                        if not line:
518                            break
519                    except:
520                        continue
521                    line_time = line[:acts_logger.log_line_timestamp_len]
522                    if not acts_logger.is_valid_logline_timestamp(line_time):
523                        continue
524                    if self._is_timestamp_in_range(line_time, begin_time,
525                        end_time):
526                        in_range = True
527                        if not line.endswith('\n'):
528                            line += '\n'
529                        out.write(line)
530                    else:
531                        if in_range:
532                            break
533
534    def start_adb_logcat(self):
535        """Starts a standing adb logcat collection in separate subprocesses and
536        save the logcat in a file.
537        """
538        if self.is_adb_logcat_on:
539            raise AndroidDeviceError(("Android device {} already has an adb "
540                                     "logcat thread going on. Cannot start "
541                                     "another one.").format(self.serial))
542        # Disable adb log spam filter.
543        self.adb.shell("logpersist.start")
544        f_name = "adblog,{},{}.txt".format(self.model, self.serial)
545        utils.create_dir(self.log_path)
546        logcat_file_path = os.path.join(self.log_path, f_name)
547        try:
548            extra_params = self.adb_logcat_param
549        except AttributeError:
550            extra_params = ""
551        cmd = "adb -s {} logcat -v threadtime {} >> {}".format(
552            self.serial, extra_params, logcat_file_path)
553        self.adb_logcat_process = utils.start_standing_subprocess(cmd)
554        self.adb_logcat_file_path = logcat_file_path
555
556    def stop_adb_logcat(self):
557        """Stops the adb logcat collection subprocess.
558        """
559        if not self.is_adb_logcat_on:
560            raise AndroidDeviceError(("Android device {} does not have an "
561                                      "ongoing adb logcat collection."
562                                      ).format(self.serial))
563        utils.stop_standing_subprocess(self.adb_logcat_process)
564        self.adb_logcat_process = None
565
566    def take_bug_report(self, test_name, begin_time):
567        """Takes a bug report on the device and stores it in a file.
568
569        Args:
570            test_name: Name of the test case that triggered this bug report.
571            begin_time: Logline format timestamp taken when the test started.
572        """
573        br_path = os.path.join(self.log_path, "BugReports")
574        utils.create_dir(br_path)
575        base_name = ",{},{}.txt".format(begin_time, self.serial)
576        test_name_len = utils.MAX_FILENAME_LEN - len(base_name)
577        out_name = test_name[:test_name_len] + base_name
578        full_out_path = os.path.join(br_path, out_name.replace(' ', '\ '))
579        self.log.info("Taking bugreport for %s on %s", test_name, self.serial)
580        self.adb.bugreport(" > {}".format(full_out_path))
581        self.log.info("Bugreport for %s taken at %s", test_name, full_out_path)
582
583    def start_new_session(self):
584        """Start a new session in sl4a.
585
586        Also caches the droid in a dict with its uid being the key.
587
588        Returns:
589            An Android object used to communicate with sl4a on the android
590                device.
591
592        Raises:
593            SL4AException: Something is wrong with sl4a and it returned an
594            existing uid to a new session.
595        """
596        droid = android.Android(port=self.h_port)
597        if droid.uid in self._droid_sessions:
598            raise android.SL4AException(("SL4A returned an existing uid for a "
599                "new session. Abort."))
600        self._droid_sessions[droid.uid] = [droid]
601        return droid
602
603    def add_new_connection_to_session(self, session_id):
604        """Create a new connection to an existing sl4a session.
605
606        Args:
607            session_id: UID of the sl4a session to add connection to.
608
609        Returns:
610            An Android object used to communicate with sl4a on the android
611                device.
612
613        Raises:
614            DoesNotExistError: Raised if the session it's trying to connect to
615            does not exist.
616        """
617        if session_id not in self._droid_sessions:
618            raise DoesNotExistError("Session %d doesn't exist." % session_id)
619        droid = android.Android(cmd='continue', uid=session_id,
620            port=self.h_port)
621        return droid
622
623    def terminate_session(self, session_id):
624        """Terminate a session in sl4a.
625
626        Send terminate signal to sl4a server; stop dispatcher associated with
627        the session. Clear corresponding droids and dispatchers from cache.
628
629        Args:
630            session_id: UID of the sl4a session to terminate.
631        """
632        if self._droid_sessions and (session_id in self._droid_sessions):
633            for droid in self._droid_sessions[session_id]:
634                droid.closeSl4aSession()
635                droid.close()
636            del self._droid_sessions[session_id]
637        ed_key = self.serial + str(session_id)
638        if ed_key in self._event_dispatchers:
639            self._event_dispatchers[ed_key].clean_up()
640            del self._event_dispatchers[ed_key]
641
642    def terminate_all_sessions(self):
643        """Terminate all sl4a sessions on the AndroidDevice instance.
644
645        Terminate all sessions and clear caches.
646        """
647        if self._droid_sessions:
648            session_ids = list(self._droid_sessions.keys())
649            for session_id in session_ids:
650                try:
651                    self.terminate_session(session_id)
652                except:
653                    msg = "Failed to terminate session %d." % session_id
654                    self.log.exception(msg)
655                    self.log.error(traceback.format_exc())
656            if self.h_port:
657                self.adb.forward("--remove tcp:%d" % self.h_port)
658                self.h_port = None
659
660    def run_iperf_client(self, server_host, extra_args=""):
661        """Start iperf client on the device.
662
663        Return status as true if iperf client start successfully.
664        And data flow information as results.
665
666        Args:
667            server_host: Address of the iperf server.
668            extra_args: A string representing extra arguments for iperf client,
669                e.g. "-i 1 -t 30".
670
671        Returns:
672            status: true if iperf client start successfully.
673            results: results have data flow information
674        """
675        out = self.adb.shell("iperf3 -c {} {}".format(server_host, extra_args))
676        clean_out = str(out,'utf-8').strip().split('\n')
677        if "error" in clean_out[0].lower():
678            return False, clean_out
679        return True, clean_out
680
681    @utils.timeout(15 * 60)
682    def wait_for_boot_completion(self):
683        """Waits for the Android framework to boot back up and ready to launch
684        apps.
685
686        This function times out after 15 minutes.
687        """
688        self.adb.wait_for_device()
689        while True:
690            try:
691                out = self.adb.shell("getprop sys.boot_completed")
692                completed = out.decode('utf-8').strip()
693                if completed == '1':
694                    return
695            except adb.AdbError:
696                # adb shell calls may fail during certain period of booting
697                # process, which is normal. Ignoring these errors.
698                pass
699            time.sleep(5)
700
701    def reboot(self):
702        """Reboots the device.
703
704        Terminate all sl4a sessions, reboot the device, wait for device to
705        complete booting, and restart an sl4a session.
706
707        This is a blocking method.
708
709        This is probably going to print some error messages in console. Only
710        use if there's no other option.
711
712        Example:
713            droid, ed = ad.reboot()
714
715        Returns:
716            An sl4a session with an event_dispatcher.
717
718        Raises:
719            AndroidDeviceError is raised if waiting for completion timed
720            out.
721        """
722        if self.is_bootloader:
723            self.fastboot.reboot()
724            return
725        has_adb_log = self.is_adb_logcat_on
726        if has_adb_log:
727            self.stop_adb_logcat()
728        self.terminate_all_sessions()
729        self.adb.reboot()
730        self.wait_for_boot_completion()
731        self.root_adb()
732        droid, ed = self.get_droid()
733        ed.start()
734        if has_adb_log:
735            self.start_adb_logcat()
736        return droid, ed
737