VTS integration framework initial refactor.

Bug=28004487

Commiting the integraton framework in the directory structure as
discussed offline.

Change-Id: I0314431e3b906a195290cd9b4630c93e07b494ca
diff --git a/utils/python/controllers/android_device.py b/utils/python/controllers/android_device.py
new file mode 100644
index 0000000..17ff80b
--- /dev/null
+++ b/utils/python/controllers/android_device.py
@@ -0,0 +1,731 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from builtins import str
+from builtins import open
+
+import os
+import time
+import traceback
+
+from vts.runners.host import logger as vts_logger
+from vts.runners.host import signals
+from vts.runners.host import utils
+from vts.runners.utils.pythoncontrollers import adb
+from vts.runners.utils.pythoncontrollers import android
+from vts.runners.utils.pythoncontrollers import event_dispatcher
+from vts.runners.utils.pythoncontrollers import fastboot
+
+VTS_CONTROLLER_CONFIG_NAME = "AndroidDevice"
+VTS_CONTROLLER_REFERENCE_NAME = "android_devices"
+
+ANDROID_DEVICE_PICK_ALL_TOKEN = "*"
+# Key name for adb logcat extra params in config file.
+ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = "adb_logcat_param"
+ANDROID_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
+ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
+
+class AndroidDeviceError(signals.ControllerError):
+    pass
+
+def create(configs, logger):
+    if not configs:
+        raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
+    elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
+        ads = get_all_instances(logger=logger)
+    elif not isinstance(configs, list):
+        raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
+    elif isinstance(configs[0], str):
+        # Configs is a list of serials.
+        ads = get_instances(configs, logger)
+    else:
+        # Configs is a list of dicts.
+        ads = get_instances_with_configs(configs, logger)
+    connected_ads = list_adb_devices()
+    for ad in ads:
+        if ad.serial not in connected_ads:
+            raise AndroidDeviceError(("Android device %s is specified in config"
+                                     " but is not attached.") % ad.serial)
+        ad.startAdbLogcat()
+        try:
+            ad.getSl4aClient()
+            ad.ed.start()
+        except:
+            # This exception is logged here to help with debugging under py2,
+            # because "exception raised while processing another exception" is
+            # only printed under py3.
+            msg = "Failed to start sl4a on %s" % ad.serial
+            logger.exception(msg)
+            raise AndroidDeviceError(msg)
+    return ads
+
+def destroy(ads):
+    for ad in ads:
+        try:
+            ad.closeAllSl4aSession()
+        except:
+            pass
+        if ad.adb_logcat_process:
+            ad.stopAdbLogcat()
+
+def _parse_device_list(device_list_str, key):
+    """Parses a byte string representing a list of devices. The string is
+    generated by calling either adb or fastboot.
+
+    Args:
+        device_list_str: Output of adb or fastboot.
+        key: The token that signifies a device in device_list_str.
+
+    Returns:
+        A list of android device serial numbers.
+    """
+    clean_lines = str(device_list_str, 'utf-8').strip().split('\n')
+    results = []
+    for line in clean_lines:
+        tokens = line.strip().split('\t')
+        if len(tokens) == 2 and tokens[1] == key:
+            results.append(tokens[0])
+    return results
+
+def list_adb_devices():
+    """List all android devices connected to the computer that are detected by
+    adb.
+
+    Returns:
+        A list of android device serials. Empty if there's none.
+    """
+    out = adb.AdbProxy().devices()
+    return _parse_device_list(out, "device")
+
+def list_fastboot_devices():
+    """List all android devices connected to the computer that are in in
+    fastboot mode. These are detected by fastboot.
+
+    Returns:
+        A list of android device serials. Empty if there's none.
+    """
+    out = fastboot.FastbootProxy().devices()
+    return _parse_device_list(out, "fastboot")
+
+def get_instances(serials, logger=None):
+    """Create AndroidDevice instances from a list of serials.
+
+    Args:
+        serials: A list of android device serials.
+        logger: A logger to be passed to each instance.
+
+    Returns:
+        A list of AndroidDevice objects.
+    """
+    results = []
+    for s in serials:
+        results.append(AndroidDevice(s, logger=logger))
+    return results
+
+def get_instances_with_configs(configs, logger=None):
+    """Create AndroidDevice instances from a list of json configs.
+
+    Each config should have the required key-value pair "serial".
+
+    Args:
+        configs: A list of dicts each representing the configuration of one
+            android device.
+        logger: A logger to be passed to each instance.
+
+    Returns:
+        A list of AndroidDevice objects.
+    """
+    results = []
+    for c in configs:
+        try:
+            serial = c.pop("serial")
+        except KeyError:
+            raise AndroidDeviceError(('Required value "serial" is missing in '
+                'AndroidDevice config %s.') % c)
+        ad = AndroidDevice(serial, logger=logger)
+        ad.loadConfig(c)
+        results.append(ad)
+    return results
+
+def get_all_instances(include_fastboot=False, logger=None):
+    """Create AndroidDevice instances for all attached android devices.
+
+    Args:
+        include_fastboot: Whether to include devices in bootloader mode or not.
+        logger: A logger to be passed to each instance.
+
+    Returns:
+        A list of AndroidDevice objects each representing an android device
+        attached to the computer.
+    """
+    if include_fastboot:
+        serial_list = list_adb_devices() + list_fastboot_devices()
+        return get_instances(serial_list, logger=logger)
+    return get_instances(list_adb_devices(), logger=logger)
+
+def filter_devices(ads, func):
+    """Finds the AndroidDevice instances from a list that match certain
+    conditions.
+
+    Args:
+        ads: A list of AndroidDevice instances.
+        func: A function that takes an AndroidDevice object and returns True
+            if the device satisfies the filter condition.
+
+    Returns:
+        A list of AndroidDevice instances that satisfy the filter condition.
+    """
+    results = []
+    for ad in ads:
+        if func(ad):
+            results.append(ad)
+    return results
+
+def get_device(ads, **kwargs):
+    """Finds a unique AndroidDevice instance from a list that has specific
+    attributes of certain values.
+
+    Example:
+        get_device(android_devices, label="foo", phone_number="1234567890")
+        get_device(android_devices, model="angler")
+
+    Args:
+        ads: A list of AndroidDevice instances.
+        kwargs: keyword arguments used to filter AndroidDevice instances.
+
+    Returns:
+        The target AndroidDevice instance.
+
+    Raises:
+        AndroidDeviceError is raised if none or more than one device is
+        matched.
+    """
+    def _get_device_filter(ad):
+        for k, v in kwargs.items():
+            if not hasattr(ad, k):
+                return False
+            elif getattr(ad, k) != v:
+                return False
+        return True
+    filtered = filter_devices(ads, _get_device_filter)
+    if not filtered:
+        raise AndroidDeviceError(("Could not find a target device that matches"
+                                  " condition: %s.") % kwargs)
+    elif len(filtered) == 1:
+        return filtered[0]
+    else:
+        serials = [ad.serial for ad in filtered]
+        raise AndroidDeviceError("More than one device matched: %s" % serials)
+
+def takeBugReports(ads, test_name, begin_time):
+    """Takes bug reports on a list of android devices.
+
+    If you want to take a bug report, call this function with a list of
+    android_device objects in on_fail. But reports will be taken on all the
+    devices in the list concurrently. Bug report takes a relative long
+    time to take, so use this cautiously.
+
+    Args:
+        ads: A list of AndroidDevice instances.
+        test_name: Name of the test case that triggered this bug report.
+        begin_time: Logline format timestamp taken when the test started.
+    """
+    begin_time = vts_logger.normalizeLogLineTimestamp(begin_time)
+    def take_br(test_name, begin_time, ad):
+        ad.takeBugReport(test_name, begin_time)
+    args = [(test_name, begin_time, ad) for ad in ads]
+    utils.concurrent_exec(take_br, args)
+
+class AndroidDevice:
+    """Class representing an android device.
+
+    Each object of this class represents one Android device in ACTS, including
+    handles to adb, fastboot, and sl4a clients. In addition to direct adb
+    commands, this object also uses adb port forwarding to talk to the Android
+    device.
+
+    Attributes:
+        serial: A string that's the serial number of the Androi device.
+        h_port: An integer that's the port number for adb port forwarding used
+                on the computer the Android device is connected
+        d_port: An integer  that's the port number used on the Android device
+                for adb port forwarding.
+        log: A LoggerProxy object used for the class's internal logging.
+        log_path: A string that is the path where all logs collected on this
+                  android device should be stored.
+        adb_logcat_process: A process that collects the adb logcat.
+        adb_logcat_file_path: A string that's the full path to the adb logcat
+                              file collected, if any.
+        adb: An AdbProxy object used for interacting with the device via adb.
+        fastboot: A FastbootProxy object used for interacting with the device
+                  via fastboot.
+    """
+
+    def __init__(self, serial="", host_port=None, device_port=8080,
+                 logger=None):
+        self.serial = serial
+        self.h_port = host_port
+        self.d_port = device_port
+        self.log = logging.getLogger()
+        lp = self.log.log_path
+        self.log_path = os.path.join(lp, "AndroidDevice%s" % serial)
+        self._droid_sessions = {}
+        self._event_dispatchers = {}
+        self.adb_logcat_process = None
+        self.adb_logcat_file_path = None
+        self.adb = adb.AdbProxy(serial)
+        self.fastboot = fastboot.FastbootProxy(serial)
+        if not self.isBootloaderMode:
+            self.rootAdb()
+
+    def __del__(self):
+        if self.h_port:
+            self.adb.forward("--remove tcp:%d" % self.h_port)
+        if self.adb_logcat_process:
+            self.stopAdbLogcat()
+
+    @property
+    def isBootloaderMode(self):
+        """True if the device is in bootloader mode.
+        """
+        return self.serial in list_fastboot_devices()
+
+    @property
+    def isAdbRoot(self):
+        """True if adb is running as root for this device.
+        """
+        return "root" in self.adb.shell("id -u").decode("utf-8")
+
+    @property
+    def model(self):
+        """The Android code name for the device.
+        """
+        # If device is in bootloader mode, get mode name from fastboot.
+        if self.isBootloaderMode:
+            out = self.fastboot.getvar("product").strip()
+            # "out" is never empty because of the "total time" message fastboot
+            # writes to stderr.
+            lines = out.decode("utf-8").split('\n', 1)
+            if lines:
+                tokens = lines[0].split(' ')
+                if len(tokens) > 1:
+                    return tokens[1].lower()
+            return None
+        out = self.adb.shell('getprop | grep ro.build.product')
+        model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
+        if model == "sprout":
+            return model
+        else:
+            out = self.adb.shell('getprop | grep ro.product.name')
+            model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
+            return model
+
+    @property
+    def droid(self):
+        """The first sl4a session initiated on this device. None if there isn't
+        one.
+        """
+        try:
+            session_id = sorted(self._droid_sessions)[0]
+            return self._droid_sessions[session_id][0]
+        except IndexError:
+            return None
+
+    @property
+    def ed(self):
+        """The first event_dispatcher instance created on this device. None if
+        there isn't one.
+        """
+        try:
+            session_id = sorted(self._event_dispatchers)[0]
+            return self._event_dispatchers[session_id]
+        except IndexError:
+            return None
+
+    @property
+    def droids(self):
+        """A list of the active sl4a sessions on this device.
+
+        If multiple connections exist for the same session, only one connection
+        is listed.
+        """
+        keys = sorted(self._droid_sessions)
+        results = []
+        for k in keys:
+            results.append(self._droid_sessions[k][0])
+        return results
+
+    @property
+    def eds(self):
+        """A list of the event_dispatcher objects on this device.
+
+        The indexing of the list matches that of the droids property.
+        """
+        keys = sorted(self._event_dispatchers)
+        results = []
+        for k in keys:
+            results.append(self._event_dispatchers[k])
+        return results
+
+    @property
+    def isAdbLogcatOn(self):
+        """Whether there is an ongoing adb logcat collection.
+        """
+        if self.adb_logcat_process:
+            return True
+        return False
+
+    def loadConfig(self, config):
+        """Add attributes to the AndroidDevice object based on json config.
+
+        Args:
+            config: A dictionary representing the configs.
+
+        Raises:
+            AndroidDeviceError is raised if the config is trying to overwrite
+            an existing attribute.
+        """
+        for k, v in config.items():
+            if hasattr(self, k):
+                raise AndroidDeviceError(("Attempting to set existing "
+                    "attribute %s on %s") % (k, self.serial))
+            setattr(self, k, v)
+
+    def rootAdb(self):
+        """Change adb to root mode for this device.
+        """
+        if not self.isAdbRoot:
+            self.adb.root()
+            self.adb.wait_for_device()
+            self.adb.remount()
+            self.adb.wait_for_device()
+
+    def getSl4aClient(self, handle_event=True):
+        """Create an sl4a connection to the device.
+
+        Return the connection handler 'droid'. By default, another connection
+        on the same session is made for EventDispatcher, and the dispatcher is
+        returned to the caller as well.
+        If sl4a server is not started on the device, try to start it.
+
+        Args:
+            handle_event: True if this droid session will need to handle
+                events.
+
+        Returns:
+            droid: Android object used to communicate with sl4a on the android
+                device.
+            ed: An optional EventDispatcher to organize events for this droid.
+
+        Examples:
+            Don't need event handling:
+            >>> ad = AndroidDevice()
+            >>> droid = ad.getSl4aClient(False)
+
+            Need event handling:
+            >>> ad = AndroidDevice()
+            >>> droid, ed = ad.getSl4aClient()
+        """
+        if not self.h_port or not adb.is_port_available(self.h_port):
+            self.h_port = adb.get_available_host_port()
+        self.adb.tcp_forward(self.h_port, self.d_port)
+        try:
+            droid = self.start_new_session()
+        except:
+            self.adb.start_sl4a()
+            droid = self.start_new_session()
+        if handle_event:
+            ed = self.getSl4aEventDispatcher(droid)
+            return droid, ed
+        return droid
+
+    def getSl4aEventDispatcher(self, droid):
+        """Return an EventDispatcher for an sl4a session
+
+        Args:
+            droid: Session to create EventDispatcher for.
+
+        Returns:
+            ed: An EventDispatcher for specified session.
+        """
+        ed_key = self.serial + str(droid.uid)
+        if ed_key in self._event_dispatchers:
+            if self._event_dispatchers[ed_key] is None:
+                raise AndroidDeviceError("EventDispatcher Key Empty")
+            self.log.debug("Returning existing key %s for event dispatcher!",
+                           ed_key)
+            return self._event_dispatchers[ed_key]
+        event_droid = self.add_new_connection_to_session(droid.uid)
+        ed = event_dispatcher.EventDispatcher(event_droid)
+        self._event_dispatchers[ed_key] = ed
+        return ed
+
+    def _is_timestamp_in_range(self, target, begin_time, end_time):
+        low = vts_logger.logLineTimestampComparator(begin_time, target) <= 0
+        high = vts_logger.logLineTimestampComparator(end_time, target) >= 0
+        return low and high
+
+    def takeAdbLogExcerpt(self, tag, begin_time):
+        """Takes an excerpt of the adb logcat log from a certain time point to
+        current time.
+
+        Args:
+            tag: An identifier of the time period, usualy the name of a test.
+            begin_time: Logline format timestamp of the beginning of the time
+                period.
+        """
+        if not self.adb_logcat_file_path:
+            raise AndroidDeviceError(("Attempting to cat adb log when none has"
+                                      " been collected on Android device %s."
+                                      ) % self.serial)
+        end_time = vts_logger.getLogLineTimestamp()
+        self.log.debug("Extracting adb log from logcat.")
+        adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts")
+        utils.create_dir(adb_excerpt_path)
+        f_name = os.path.basename(self.adb_logcat_file_path)
+        out_name = f_name.replace("adblog,", "").replace(".txt", "")
+        out_name = ",{},{}.txt".format(begin_time, out_name)
+        tag_len = utils.MAX_FILENAME_LEN - len(out_name)
+        tag = tag[:tag_len]
+        out_name = tag + out_name
+        full_adblog_path = os.path.join(adb_excerpt_path, out_name)
+        with open(full_adblog_path, 'w', encoding='utf-8') as out:
+            in_file = self.adb_logcat_file_path
+            with open(in_file, 'r', encoding='utf-8', errors='replace') as f:
+                in_range = False
+                while True:
+                    line = None
+                    try:
+                        line = f.readline()
+                        if not line:
+                            break
+                    except:
+                        continue
+                    line_time = line[:vts_logger.log_line_timestamp_len]
+                    if not vts_logger.isValidLogLineTimestamp(line_time):
+                        continue
+                    if self._is_timestamp_in_range(line_time, begin_time,
+                        end_time):
+                        in_range = True
+                        if not line.endswith('\n'):
+                            line += '\n'
+                        out.write(line)
+                    else:
+                        if in_range:
+                            break
+
+    def startAdbLogcat(self):
+        """Starts a standing adb logcat collection in separate subprocesses and
+        save the logcat in a file.
+        """
+        if self.isAdbLogcatOn:
+            raise AndroidDeviceError(("Android device {} already has an adb "
+                                     "logcat thread going on. Cannot start "
+                                     "another one.").format(self.serial))
+        # Disable adb log spam filter.
+        self.adb.shell("logpersist.start")
+        f_name = "adblog,{},{}.txt".format(self.model, self.serial)
+        utils.create_dir(self.log_path)
+        logcat_file_path = os.path.join(self.log_path, f_name)
+        try:
+            extra_params = self.adb_logcat_param
+        except AttributeError:
+            extra_params = "-b all"
+        cmd = "adb -s {} logcat -v threadtime {} >> {}".format(
+            self.serial, extra_params, logcat_file_path)
+        self.adb_logcat_process = utils.start_standing_subprocess(cmd)
+        self.adb_logcat_file_path = logcat_file_path
+
+    def stopAdbLogcat(self):
+        """Stops the adb logcat collection subprocess.
+        """
+        if not self.isAdbLogcatOn:
+            raise AndroidDeviceError(("Android device {} does not have an "
+                                      "ongoing adb logcat collection."
+                                      ).format(self.serial))
+        utils.stop_standing_subprocess(self.adb_logcat_process)
+        self.adb_logcat_process = None
+
+    def takeBugReport(self, test_name, begin_time):
+        """Takes a bug report on the device and stores it in a file.
+
+        Args:
+            test_name: Name of the test case that triggered this bug report.
+            begin_time: Logline format timestamp taken when the test started.
+        """
+        br_path = os.path.join(self.log_path, "BugReports")
+        utils.create_dir(br_path)
+        base_name = ",{},{}.txt".format(begin_time, self.serial)
+        test_name_len = utils.MAX_FILENAME_LEN - len(base_name)
+        out_name = test_name[:test_name_len] + base_name
+        full_out_path = os.path.join(br_path, out_name.replace(' ', '\ '))
+        self.log.info("Taking bugreport for %s on %s", test_name, self.serial)
+        self.adb.bugreport(" > {}".format(full_out_path))
+        self.log.info("Bugreport for %s taken at %s", test_name, full_out_path)
+
+    def start_new_session(self):
+        """Start a new session in sl4a.
+
+        Also caches the droid in a dict with its uid being the key.
+
+        Returns:
+            An Android object used to communicate with sl4a on the android
+                device.
+
+        Raises:
+            SL4AException: Something is wrong with sl4a and it returned an
+            existing uid to a new session.
+        """
+        droid = android.Android(port=self.h_port)
+        if droid.uid in self._droid_sessions:
+            raise android.SL4AException(("SL4A returned an existing uid for a "
+                "new session. Abort."))
+        self._droid_sessions[droid.uid] = [droid]
+        return droid
+
+    def add_new_connection_to_session(self, session_id):
+        """Create a new connection to an existing sl4a session.
+
+        Args:
+            session_id: UID of the sl4a session to add connection to.
+
+        Returns:
+            An Android object used to communicate with sl4a on the android
+                device.
+
+        Raises:
+            AndroidDeviceError: Raised if the session it's trying to connect to
+            does not exist.
+        """
+        if session_id not in self._droid_sessions:
+            raise AndroidDeviceError("Session %d doesn't exist." % session_id)
+        droid = android.Android(cmd='continue', uid=session_id,
+            port=self.h_port)
+        return droid
+
+    def closeOneSl4aSession(self, session_id):
+        """Terminate a session in sl4a.
+
+        Send terminate signal to sl4a server; stop dispatcher associated with
+        the session. Clear corresponding droids and dispatchers from cache.
+
+        Args:
+            session_id: UID of the sl4a session to terminate.
+        """
+        if self._droid_sessions and (session_id in self._droid_sessions):
+            for droid in self._droid_sessions[session_id]:
+                droid.closeSl4aSession()
+                droid.close()
+            del self._droid_sessions[session_id]
+        ed_key = self.serial + str(session_id)
+        if ed_key in self._event_dispatchers:
+            self._event_dispatchers[ed_key].clean_up()
+            del self._event_dispatchers[ed_key]
+
+    def closeAllSl4aSession(self):
+        """Terminate all sl4a sessions on the AndroidDevice instance.
+
+        Terminate all sessions and clear caches.
+        """
+        if self._droid_sessions:
+            session_ids = list(self._droid_sessions.keys())
+            for session_id in session_ids:
+                try:
+                    self.closeOneSl4aSession(session_id)
+                except:
+                    msg = "Failed to terminate session %d." % session_id
+                    self.log.exception(msg)
+                    self.log.error(traceback.format_exc())
+            if self.h_port:
+                self.adb.forward("--remove tcp:%d" % self.h_port)
+                self.h_port = None
+
+    def runIperfClient(self, server_host, extra_args=""):
+        """Start iperf client on the device.
+
+        Return status as true if iperf client start successfully.
+        And data flow information as results.
+
+        Args:
+            server_host: Address of the iperf server.
+            extra_args: A string representing extra arguments for iperf client,
+                e.g. "-i 1 -t 30".
+
+        Returns:
+            status: true if iperf client start successfully.
+            results: results have data flow information
+        """
+        out = self.adb.shell("iperf3 -c {} {}".format(server_host, extra_args))
+        clean_out = str(out,'utf-8').strip().split('\n')
+        if "error" in clean_out[0].lower():
+            return False, clean_out
+        return True, clean_out
+
+    @utils.timeout(15 * 60)
+    def waitForBootCompletion(self):
+        """Waits for Android framework to broadcast ACTION_BOOT_COMPLETED.
+
+        This function times out after 15 minutes.
+        """
+        self.adb.wait_for_device()
+        while True:
+            try:
+                out = self.adb.shell("getprop sys.boot_completed")
+                completed = out.decode('utf-8').strip()
+                if completed == '1':
+                    return
+            except adb.AdbError:
+                # adb shell calls may fail during certain period of booting
+                # process, which is normal. Ignoring these errors.
+                pass
+            time.sleep(5)
+
+    def reboot(self):
+        """Reboots the device.
+
+        Terminate all sl4a sessions, reboot the device, wait for device to
+        complete booting, and restart an sl4a session.
+
+        This is a blocking method.
+
+        This is probably going to print some error messages in console. Only
+        use if there's no other option.
+
+        Example:
+            droid, ed = ad.reboot()
+
+        Returns:
+            An sl4a session with an event_dispatcher.
+
+        Raises:
+            AndroidDeviceError is raised if waiting for completion timed
+            out.
+        """
+        if self.isBootloaderMode:
+            self.fastboot.reboot()
+            return
+        has_adb_log = self.isAdbLogcatOn
+        if has_adb_log:
+            self.stopAdbLogcat()
+        self.closeAllSl4aSession()
+        self.adb.reboot()
+        self.waitForBootCompletion()
+        self.rootAdb()
+        droid, ed = self.getSl4aClient()
+        ed.start()
+        if has_adb_log:
+            self.startAdbLogcat()
+        return droid, ed