Add sl4a support to VTS.

Add sl4a_client to VTS.
Add the preliminary mechanism to individually turn on/off services
like VTS agent and sl4a.
Add a sample test showing how to make sl4a calls.

This enables users to control Android devices by calling Java APIs
in test cases.
SL4A can be used with or without VTS Agent. The user can specify
which service(s) to use in config file.

Bug=30899180
Test: python SampleSl4aTest.py SampleSl4aTest.config

Change-Id: I237b9e9cee1099bc2a10a05e3b8124a65d23294e
diff --git a/utils/python/controllers/android_device.py b/utils/python/controllers/android_device.py
index aa704fc..dbef29a 100644
--- a/utils/python/controllers/android_device.py
+++ b/utils/python/controllers/android_device.py
@@ -23,7 +23,6 @@
 import traceback
 import threading
 import socket
-import Queue
 
 from vts.runners.host import keys
 from vts.runners.host import logger as vts_logger
@@ -32,6 +31,7 @@
 from vts.utils.python.controllers import adb
 from vts.utils.python.controllers import event_dispatcher
 from vts.utils.python.controllers import fastboot
+from vts.utils.python.controllers import sl4a_client
 from vts.runners.host.tcp_client import vts_tcp_client
 from vts.utils.python.mirror import hal_mirror
 from vts.utils.python.mirror import shell_mirror
@@ -62,13 +62,12 @@
     pass
 
 
-def create(configs, use_vts_agent=True):
+def create(configs):
     """Creates AndroidDevice controller objects.
 
     Args:
         configs: A list of dicts, each representing a configuration for an
                  Android device.
-        use_vts_agent: bool, whether to use VTS agent.
 
     Returns:
         A list of AndroidDevice objects.
@@ -86,12 +85,11 @@
         # Configs is a list of dicts.
         ads = get_instances_with_configs(configs)
     connected_ads = list_adb_devices()
-
     for ad in ads:
         if ad.serial not in connected_ads:
             raise DoesNotExistError(("Android device %s is specified in config"
                                      " but is not attached.") % ad.serial)
-    _startServicesOnAds(ads, use_vts_agent)
+    _startServicesOnAds(ads)
     return ads
 
 
@@ -108,7 +106,7 @@
             ad.log.exception("Failed to clean up properly.")
 
 
-def _startServicesOnAds(ads, use_vts_agent):
+def _startServicesOnAds(ads):
     """Starts long running services on multiple AndroidDevice objects.
 
     If any one AndroidDevice object fails to start services, cleans up all
@@ -116,13 +114,12 @@
 
     Args:
         ads: A list of AndroidDevice objects whose services to start.
-        use_vts_agent: bool, whether to use the VTS agent.
     """
     running_ads = []
     for ad in ads:
         running_ads.append(ad)
         try:
-            ad.startServices(use_vts_agent)
+            ad.startServices()
         except:
             ad.log.exception("Failed to start some services, abort!")
             destroy(running_ads)
@@ -369,13 +366,17 @@
         self.fastboot = fastboot.FastbootProxy(serial)
         if not self.isBootloaderMode:
             self.rootAdb()
-        self.host_command_port = adb.get_available_host_port()
+        self.host_command_port = None
         self.host_callback_port = adb.get_available_host_port()
         self.adb.reverse_tcp_forward(self.device_callback_port,
                                      self.host_callback_port)
         self.hal = None
         self.lib = None
         self.shell = None
+        self.sl4a_host_port = None
+        # TODO: figure out a good way to detect which port is available
+        # on the target side, instead of hard coding a port number.
+        self.sl4a_target_port = 8082
 
     def __del__(self):
         self.cleanUp()
@@ -387,6 +388,10 @@
         self.stopServices()
         if self.host_command_port:
             self.adb.forward("--remove tcp:%s" % self.host_command_port)
+            self.host_command_port = None
+        if self.sl4a_host_port:
+            self.adb.forward("--remove tcp:%s" % self.sl4a_host_port)
+            self.sl4a_host_port = None
 
     @property
     def isBootloaderMode(self):
@@ -397,7 +402,6 @@
     def isAdbRoot(self):
         """True if adb is running as root for this device."""
         id_str = self.adb.shell("id -u").decode("utf-8")
-        self.log.info(id_str)
         return "root" in id_str
 
     @property
@@ -574,33 +578,34 @@
         if has_vts_agent:
             self.startVtsAgent()
 
-    def startServices(self, use_vts_agent):
+    def startServices(self):
         """Starts long running services on the android device.
 
         1. Start adb logcat capture.
-        2. Start VtsAgent.
-        3. Create HalMirror
-
-        Args:
-            use_vts_agent: bool, whether to use the VTS agent.
+        2. Start VtsAgent and create HalMirror unless disabled in config.
+        3. If enabled in config, start sl4a service and create sl4a clients.
         """
+        enable_vts_agent = getattr(self, "enable_vts_agent", True)
+        enable_sl4a = getattr(self, "enable_sl4a", False)
         try:
             self.startAdbLogcat()
         except:
             self.log.exception("Failed to start adb logcat!")
             raise
-        if use_vts_agent:
+        if enable_vts_agent:
             self.startVtsAgent()
             self.device_command_port = int(
                 self.adb.shell("cat /data/local/tmp/vts_tcp_server_port"))
             logging.info("device_command_port: %s", self.device_command_port)
+            if not self.host_command_port:
+                self.host_command_port = adb.get_available_host_port()
             self.adb.tcp_forward(self.host_command_port, self.device_command_port)
             self.hal = hal_mirror.HalMirror(self.host_command_port,
                                             self.host_callback_port)
             self.lib = lib_mirror.LibMirror(self.host_command_port)
             self.shell = shell_mirror.ShellMirror(self.host_command_port)
-        else:
-            logging.info("not using VTS agent.")
+        if enable_sl4a:
+            self.startSl4aClient()
 
     def stopServices(self):
         """Stops long running services on the android device.
@@ -617,7 +622,7 @@
         This function starts the target side native agent and is persisted
         throughout the test run.
         """
-        self.log.info("start a VTS agent")
+        self.log.info("Starting VTS agent")
         if self.vts_agent_process:
             raise AndroidDeviceError("HAL agent is already running on %s." %
                                      self.serial)
@@ -687,6 +692,135 @@
         """Gets the product type name."""
         return self._product_type
 
+    # Code for using SL4A client
+    def startSl4aClient(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.
+        """
+        self._sl4a_sessions = {}
+        self._sl4a_event_dispatchers = {}
+        if not self.sl4a_host_port or not adb.is_port_available(self.sl4a_host_port):
+            self.sl4a_host_port = adb.get_available_host_port()
+        self.adb.tcp_forward(self.sl4a_host_port, self.sl4a_target_port)
+        try:
+            droid = self._createNewSl4aSession()
+        except sl4a_client.Error:
+            sl4a_client.start_sl4a(self.adb)
+            droid = self._createNewSl4aSession()
+        self.sl4a = droid
+        if handle_event:
+            ed = self._getSl4aEventDispatcher(droid)
+        self.sl4a_event = ed
+
+    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.
+        """
+        # TODO (angli): Move service-specific start/stop functions out of
+        # android_device, including VTS Agent, SL4A, and any other
+        # target-side services.
+        ed_key = self.serial + str(droid.uid)
+        if ed_key in self._sl4a_event_dispatchers:
+            if self._sl4a_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._sl4a_event_dispatchers[ed_key]
+        event_droid = self._addNewConnectionToSl4aSession(droid.uid)
+        ed = event_dispatcher.EventDispatcher(event_droid)
+        self._sl4a_event_dispatchers[ed_key] = ed
+        return ed
+
+    def _createNewSl4aSession(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:
+            sl4a_client.Error: Something is wrong with sl4a and it returned an
+            existing uid to a new session.
+        """
+        droid = sl4a_client.Sl4aClient(port=self.sl4a_host_port)
+        droid.open()
+        if droid.uid in self._sl4a_sessions:
+            raise sl4a_client.Error(
+                "SL4A returned an existing uid for a new session. Abort.")
+        self._sl4a_sessions[droid.uid] = [droid]
+        return droid
+
+    def _addNewConnectionToSl4aSession(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:
+            DoesNotExistError: Raised if the session it's trying to connect to
+            does not exist.
+        """
+        if session_id not in self._sl4a_sessions:
+            raise DoesNotExistError("Session %d doesn't exist." % session_id)
+        droid = sl4a_client.Sl4aClient(port=self.sl4a_host_port, uid=session_id)
+        droid.open(cmd=sl4a_client.Sl4aCommand.CONTINUE)
+        return droid
+
+    def _terminateSl4aSession(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._sl4a_sessions and (session_id in self._sl4a_sessions):
+            for droid in self._sl4a_sessions[session_id]:
+                droid.closeSl4aSession()
+                droid.close()
+            del self._sl4a_sessions[session_id]
+        ed_key = self.serial + str(session_id)
+        if ed_key in self._sl4a_event_dispatchers:
+            self._sl4a_event_dispatchers[ed_key].clean_up()
+            del self._sl4a_event_dispatchers[ed_key]
+
+    def _terminateAllSl4aSessions(self):
+        """Terminate all sl4a sessions on the AndroidDevice instance.
+
+        Terminate all sessions and clear caches.
+        """
+        if self._sl4a_sessions:
+            session_ids = list(self._sl4a_sessions.keys())
+            for session_id in session_ids:
+                try:
+                    self._terminateSl4aSession(session_id)
+                except:
+                    self.log.exception("Failed to terminate session %d.",
+                                       session_id)
+            if self.sl4a_host_port:
+                self.adb.forward("--remove tcp:%d" % self.sl4a_host_port)
+                self.sl4a_host_port = None
+
 
 class AndroidDeviceLoggerAdapter(logging.LoggerAdapter):
     """A wrapper class that attaches a prefix to all log lines from an
diff --git a/utils/python/controllers/sl4a_client.py b/utils/python/controllers/sl4a_client.py
new file mode 100644
index 0000000..7ecd3b4
--- /dev/null
+++ b/utils/python/controllers/sl4a_client.py
@@ -0,0 +1,274 @@
+#/usr/bin/env python3.4
+#
+# Copyright (C) 2009 Google Inc.
+#
+# 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.
+"""
+JSON RPC interface to android scripting engine.
+"""
+
+from builtins import str
+
+import json
+import logging
+import os
+import socket
+import sys
+import threading
+import time
+
+from vts.utils.python.controllers import adb
+
+HOST = os.environ.get('SL4A_HOST_ADDRESS', None)
+PORT = os.environ.get('SL4A_HOST_PORT', 9999)
+DEFAULT_DEVICE_SIDE_PORT = 8080
+
+UNKNOWN_UID = -1
+
+MAX_SL4A_WAIT_TIME = 10
+_SL4A_LAUNCH_CMD = (
+    "am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER "
+    "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT {} "
+    "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher")
+
+
+class Error(Exception):
+    pass
+
+
+class StartError(Error):
+    """Raised when sl4a is not able to be started."""
+
+
+class ApiError(Error):
+    """Raised when remote API reports an error."""
+
+
+class ProtocolError(Error):
+    """Raised when there is some error in exchanging data with server on device."""
+    NO_RESPONSE_FROM_HANDSHAKE = "No response from handshake."
+    NO_RESPONSE_FROM_SERVER = "No response from server."
+    MISMATCHED_API_ID = "Mismatched API id."
+
+
+def start_sl4a(adb_proxy,
+               device_side_port=DEFAULT_DEVICE_SIDE_PORT,
+               wait_time=MAX_SL4A_WAIT_TIME):
+    """Starts sl4a server on the android device.
+
+    Args:
+        adb_proxy: adb.AdbProxy, The adb proxy to use to start sl4a
+        device_side_port: int, The port number to open on the device side.
+        wait_time: float, The time to wait for sl4a to come up before raising
+                   an error.
+
+    Raises:
+        Error: Raised when SL4A was not able to be started.
+    """
+    if not is_sl4a_installed(adb_proxy):
+        raise StartError("SL4A is not installed on %s" % adb_proxy.serial)
+    MAX_SL4A_WAIT_TIME = 10
+    adb_proxy.shell(_SL4A_LAUNCH_CMD.format(device_side_port))
+    for _ in range(wait_time):
+        time.sleep(1)
+        if is_sl4a_running(adb_proxy):
+            return
+    raise StartError("SL4A failed to start on %s." % adb_proxy.serial)
+
+
+def is_sl4a_installed(adb_proxy):
+    """Checks if sl4a is installed by querying the package path of sl4a.
+
+    Args:
+        adb: adb.AdbProxy, The adb proxy to use for checking install.
+
+    Returns:
+        True if sl4a is installed, False otherwise.
+    """
+    try:
+        adb_proxy.shell("pm path com.googlecode.android_scripting")
+        return True
+    except adb.AdbError as e:
+        if not e.stderr:
+            return False
+        raise
+
+
+def is_sl4a_running(adb_proxy):
+    """Checks if the sl4a app is running on an android device.
+
+    Args:
+        adb_proxy: adb.AdbProxy, The adb proxy to use for checking.
+
+    Returns:
+        True if the sl4a app is running, False otherwise.
+    """
+    # Grep for process with a preceding S which means it is truly started.
+    out = adb_proxy.shell('ps | grep "S com.googlecode.android_scripting"')
+    if len(out) == 0:
+        return False
+    return True
+
+
+class Sl4aCommand(object):
+    """Commands that can be invoked on the sl4a client.
+
+    INIT: Initializes a new sessions in sl4a.
+    CONTINUE: Creates a connection.
+    """
+    INIT = 'initiate'
+    CONTINUE = 'continue'
+
+
+class Sl4aClient(object):
+    """A sl4a client that is connected to remotely.
+
+    Connects to a remove device running sl4a. Before opening a connection
+    a port forward must be setup to go over usb. This be done using
+    adb.tcp_forward(). This is calling the shell command
+    adb forward <local> remote>. Once the port has been forwarded it can be
+    used in this object as the port of communication.
+
+    Attributes:
+        port: int, The host port to communicate through.
+        addr: str, The host address who is communicating to the device (usually
+                   localhost).
+        client: file, The socket file used to communicate.
+        uid: int, The sl4a uid of this session.
+        conn: socket.Socket, The socket connection to the remote client.
+    """
+
+    _SOCKET_TIMEOUT = 60
+
+    def __init__(self, port=PORT, addr=HOST, uid=UNKNOWN_UID):
+        """
+        Args:
+            port: int, The port this client should connect to.
+            addr: str, The address this client should connect to.
+            uid: int, The uid of the session to join, or UNKNOWN_UID to start a
+                 new session.
+        """
+        self.port = port
+        self.addr = addr
+        self._lock = threading.Lock()
+        self.client = None  # prevent close errors on connect failure
+        self.uid = uid
+        self.conn = None
+
+    def __del__(self):
+        self.close()
+
+    def _id_counter(self):
+        i = 0
+        while True:
+            yield i
+            i += 1
+
+    def open(self, cmd=Sl4aCommand.INIT):
+        """Opens a connection to the remote client.
+
+        Opens a connection to a remote client with sl4a. The connection will
+        error out if it takes longer than the connection_timeout time. Once
+        connected if the socket takes longer than _SOCKET_TIMEOUT to respond
+        the connection will be closed.
+
+        Args:
+            cmd: Sl4aCommand, The command to use for creating the connection.
+
+        Raises:
+            IOError: Raised when the socket times out from io error
+            socket.timeout: Raised when the socket waits to long for connection.
+            ProtocolError: Raised when there is an error in the protocol.
+        """
+        self._counter = self._id_counter()
+        try:
+            self.conn = socket.create_connection((self.addr, self.port), 30)
+        except (socket.timeout):
+            logging.exception("Failed to create socket connection!")
+            raise
+        self.client = self.conn.makefile(mode="brw")
+        resp = self._cmd(cmd, self.uid)
+        if not resp:
+            raise ProtocolError(
+                ProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
+        result = json.loads(str(resp, encoding="utf8"))
+        if result['status']:
+            self.uid = result['uid']
+        else:
+            self.uid = UNKNOWN_UID
+
+    def close(self):
+        """Close the connection to the remote client."""
+        if self.conn is not None:
+            self.conn.close()
+            self.conn = None
+
+    def _cmd(self, command, uid=None):
+        """Send a command to sl4a.
+
+        Given a command name, this will package the command and send it to
+        sl4a.
+
+        Args:
+            command: str, The name of the command to execute.
+            uid: int, the uid of the session to send the command to.
+
+        Returns:
+            The line that was written back.
+        """
+        if not uid:
+            uid = self.uid
+        self.client.write(json.dumps({'cmd': command,
+                                      'uid': uid}).encode("utf8") + b'\n')
+        self.client.flush()
+        return self.client.readline()
+
+    def _rpc(self, method, *args):
+        """Sends an rpc to sl4a.
+
+        Sends an rpc call to sl4a using this clients connection.
+
+        Args:
+            method: str, The name of the method to execute.
+            args: any, The args to send to sl4a.
+
+        Returns:
+            The result of the rpc.
+
+        Raises:
+            ProtocolError: Something went wrong with the sl4a protocol.
+            ApiError: The rpc went through, however executed with errors.
+        """
+        with self._lock:
+            apiid = next(self._counter)
+        data = {'id': apiid, 'method': method, 'params': args}
+        request = json.dumps(data)
+        self.client.write(request.encode("utf8") + b'\n')
+        self.client.flush()
+        response = self.client.readline()
+        if not response:
+            raise ProtocolError(ProtocolError.NO_RESPONSE_FROM_SERVER)
+        result = json.loads(str(response, encoding="utf8"))
+        if result['error']:
+            raise ApiError(result['error'])
+        if result['id'] != apiid:
+            raise ProtocolError(ProtocolError.MISMATCHED_API_ID)
+        return result['result']
+
+    def __getattr__(self, name):
+        """Wrapper for python magic to turn method calls into RPC calls."""
+
+        def rpc_call(*args):
+            return self._rpc(name, *args)
+
+        return rpc_call