Sl4a as a library support

Make Sl4A a library support managed by test writters.

Test: unit tests, run google/example/sl4a-BT-discovery
Bug: 31921775
Change-Id: Ib0a869dd4c8eff094e1791ccc72b53d67e07f69b
diff --git a/prod-tests/src/com/android/tradefed/Sl4aBluetoothDiscovery.java b/prod-tests/src/com/android/tradefed/Sl4aBluetoothDiscovery.java
index c45d1f7..86a79d4 100644
--- a/prod-tests/src/com/android/tradefed/Sl4aBluetoothDiscovery.java
+++ b/prod-tests/src/com/android/tradefed/Sl4aBluetoothDiscovery.java
@@ -90,8 +90,10 @@
 
     @Override
     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
-        Sl4aClient clientDUT = mDut.startSL4A();
-        Sl4aClient clientDiscoverer = mDiscoverer.startSL4A();
+        // We provide null path for the apk to assume it's already installed.
+        Sl4aClient dutClient = Sl4aClient.startSL4A(mDut, null);
+        Sl4aClient discovererClient = Sl4aClient.startSL4A(mDiscoverer, null);
+
         TestIdentifier testId = new TestIdentifier(this.getClass().getCanonicalName(),
                 "bluetooth_discovery");
 
@@ -100,17 +102,17 @@
         listener.testStarted(testId);
 
         try {
-            setup(clientDUT, clientDiscoverer);
-            clientDUT.rpcCall("bluetoothMakeDiscoverable");
-            Object rep = clientDUT.rpcCall("bluetoothGetScanMode");
+            setup(dutClient, discovererClient);
+            dutClient.rpcCall("bluetoothMakeDiscoverable");
+            Object rep = dutClient.rpcCall("bluetoothGetScanMode");
             // 3 signifies CONNECTABLE and DISCOVERABLE
             Assert.assertEquals(3, rep);
 
-            clientDiscoverer.getEventDispatcher().clearAllEvents();
-            clientDiscoverer.rpcCall("bluetoothStartDiscovery");
-            clientDiscoverer.getEventDispatcher()
+            discovererClient.getEventDispatcher().clearAllEvents();
+            discovererClient.rpcCall("bluetoothStartDiscovery");
+            discovererClient.getEventDispatcher()
                     .popEvent("BluetoothDiscoveryFinished", 60000);
-            Object listDiscovered = clientDiscoverer.rpcCall("bluetoothGetDiscoveredDevices");
+            Object listDiscovered = discovererClient.rpcCall("bluetoothGetDiscoveredDevices");
             JSONArray response = (JSONArray) listDiscovered;
             boolean found = false;
             for (int i = 0; i < response.length(); i++) {
@@ -129,6 +131,8 @@
         } finally {
             listener.testEnded(testId, Collections.emptyMap());
             listener.testRunEnded(System.currentTimeMillis() - startTime, Collections.emptyMap());
+            dutClient.close();
+            discovererClient.close();
         }
     }
 }
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/src/com/android/tradefed/device/ITestDevice.java
index fdfb90c..4007fc6 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/src/com/android/tradefed/device/ITestDevice.java
@@ -18,7 +18,6 @@
 
 import com.android.ddmlib.IDevice;
 import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.util.sl4a.Sl4aClient;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -559,12 +558,4 @@
      * @throws DeviceNotAvailableException
      */
     public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException;
-
-    /**
-     * Create and return the sl4a client associated with the ITestDevice.
-     * Use option "--sl4a-apk-path <path>" to specify a particular apk to use.
-     *
-     * @return a {@link Sl4aClient} that is successfully connected and running on the device.
-     */
-    public Sl4aClient startSL4A() throws DeviceNotAvailableException;
 }
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 8f82fb8..eb3cd40 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -53,7 +53,6 @@
 import com.android.tradefed.util.SizeLimitedOutputStream;
 import com.android.tradefed.util.StreamUtil;
 import com.android.tradefed.util.ZipUtil2;
-import com.android.tradefed.util.sl4a.Sl4aClient;
 
 import org.apache.commons.compress.archivers.zip.ZipFile;
 
@@ -3679,12 +3678,4 @@
         CLog.d("No valid MAC address queried from device %s", mIDevice.getSerialNumber());
         return null;
     }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Sl4aClient startSL4A() throws DeviceNotAvailableException {
-        throw new UnsupportedOperationException("Sl4a is not supported by native device.");
-    }
 }
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index e82740f..a0970db 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -27,7 +27,6 @@
 import com.android.tradefed.result.InputStreamSource;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
-import com.android.tradefed.util.sl4a.Sl4aClient;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -36,7 +35,6 @@
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
-import java.net.ServerSocket;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -78,8 +76,6 @@
 
     private static final int API_LEVEL_GET_CURRENT_USER = 24;
 
-    private Sl4aClient mSl4aClient = null;
-
     /**
      * @param device
      * @param stateMonitor
@@ -1080,49 +1076,4 @@
                     + "Must be API %d.", feature, getSerialNumber(), strictMinLevel));
         }
     }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void postInvocationTearDown() {
-        super.postInvocationTearDown();
-        if (mSl4aClient != null) {
-            mSl4aClient.close();
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Sl4aClient startSL4A() throws DeviceNotAvailableException {
-        File sl4aApkFile = getOptions().getSl4aApkFile();
-        if (sl4aApkFile != null) {
-            if (!sl4aApkFile.exists()) {
-                throw new RuntimeException(String.format("Sl4A apk '%s' was not found.",
-                        sl4aApkFile.getAbsoluteFile()));
-            }
-            String res = installPackage(sl4aApkFile, true);
-            if (res != null) {
-                throw new RuntimeException(String.format("Error when installing the Sl4A apk: %s",
-                        res));
-            }
-        }
-        ServerSocket s = null;
-        int port = -1;
-        try {
-            s = new ServerSocket(0);
-            s.setReuseAddress(true);
-            port = s.getLocalPort();
-            s.close();
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-        // even after being closed, socket may remain in TIME_WAIT state
-        // reuse address allows to connect to it even in this state.
-        mSl4aClient = new Sl4aClient(this, port, 9998);
-        mSl4aClient.startSl4A();
-        return mSl4aClient;
-    }
 }
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/src/com/android/tradefed/device/TestDeviceOptions.java
index d6d81a6..5916cee 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/src/com/android/tradefed/device/TestDeviceOptions.java
@@ -17,7 +17,6 @@
 
 import com.android.tradefed.config.Option;
 
-import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -108,10 +107,6 @@
             "the minimum battery level required to continue the invocation. Scale: 0-100")
     private Integer mCutoffBattery = null;
 
-    @Option(name = "sl4a-apk-path", description = "path to the sl4a.apk file to be used by the"
-            + "device, if null it will assume the apk is already installed on the device.")
-    private File mSl4aApkFile = null;
-
     /**
      * Check whether adb root should be enabled on boot for this device
      */
@@ -348,11 +343,4 @@
     public boolean isWifiExpoRetryEnabled() {
         return mWifiExpoRetryEnabled;
     }
-
-    /**
-     * @return a {@link File} pointing to the sl4a.apk file. Can be null, if apk already installed.
-     */
-    public File getSl4aApkFile() {
-        return mSl4aApkFile;
-    }
 }
diff --git a/src/com/android/tradefed/util/sl4a/Sl4aClient.java b/src/com/android/tradefed/util/sl4a/Sl4aClient.java
index 000e9ca..8d6aa11 100644
--- a/src/com/android/tradefed/util/sl4a/Sl4aClient.java
+++ b/src/com/android/tradefed/util/sl4a/Sl4aClient.java
@@ -26,9 +26,11 @@
 import org.json.JSONObject;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
+import java.net.ServerSocket;
 import java.net.Socket;
 import java.util.HashMap;
 import java.util.Map;
@@ -36,16 +38,16 @@
 /**
  * Sl4A client to interact via RPC with SL4A scripting layer.
  */
-public class Sl4aClient {
+public class Sl4aClient implements AutoCloseable {
 
-    public static final String INIT = "initiate";
-    public static final String CONTINUE = "continue";
+    private static final String INIT = "initiate";
     public static final String IS_SL4A_RUNNING_CMD =
             "ps -e | grep \"S com.googlecode.android_scripting\"";
     public static final String SL4A_LAUNCH_CMD =
             "am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER " +
             "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT %s " +
             "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher";
+    public static final String STOP_SL4A_CMD = "am force-stop com.googlecode.android_scripting";
 
     private static final int UNKNOWN_ID = -1;
 
@@ -72,6 +74,44 @@
     }
 
     /**
+     * Convenience method to create and start a client ready to use.
+     *
+     * @param device the {ITestDevice} that the client will be for.
+     * @param sl4aApkFile file path to hte sl4a apk to install, or null if already installed.
+     * @return an {@link Sl4aClient} instance that has been started.
+     * @throws DeviceNotAvailableException
+     */
+    public static Sl4aClient startSL4A(ITestDevice device, File sl4aApkFile)
+            throws DeviceNotAvailableException {
+        if (sl4aApkFile != null) {
+            if (!sl4aApkFile.exists()) {
+                throw new RuntimeException(String.format("Sl4A apk '%s' was not found.",
+                        sl4aApkFile.getAbsoluteFile()));
+            }
+            String res = device.installPackage(sl4aApkFile, true);
+            if (res != null) {
+                throw new RuntimeException(String.format("Error when installing the Sl4A apk: %s",
+                        res));
+            }
+        }
+        ServerSocket s = null;
+        int port = -1;
+        try {
+            s = new ServerSocket(0);
+            s.setReuseAddress(true);
+            port = s.getLocalPort();
+            s.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        // even after being closed, socket may remain in TIME_WAIT state
+        // reuse address allows to connect to it even in this state.
+        Sl4aClient sl4aClient = new Sl4aClient(device, port, 9998);
+        sl4aClient.startSl4A();
+        return sl4aClient;
+    }
+
+    /**
      * Return the default runutil instance. Exposed for testing.
      */
     protected IRunUtil getRunUtil() {
@@ -189,8 +229,10 @@
     }
 
     /**
-     * Close the sl4a connection to device side.
+     * Close the sl4a connection to device side and Kills any running instance of sl4a.
+     * If no instance is running then nothing is done.
      */
+    @Override
     public void close() {
         try {
             if (mEventDispatcher != null) {
@@ -199,6 +241,7 @@
             if (mSocket != null) {
                 mSocket.close();
             }
+            mDevice.executeShellCommand(STOP_SL4A_CMD);
             mDevice.executeAdbCommand("forward", "--remove", "tcp:" + mHostPort);
         } catch (IOException | DeviceNotAvailableException e) {
             CLog.e(e);
diff --git a/src/com/android/tradefed/util/sl4a/Sl4aEventDispatcher.java b/src/com/android/tradefed/util/sl4a/Sl4aEventDispatcher.java
index 8814f63..3e3358f 100644
--- a/src/com/android/tradefed/util/sl4a/Sl4aEventDispatcher.java
+++ b/src/com/android/tradefed/util/sl4a/Sl4aEventDispatcher.java
@@ -68,7 +68,6 @@
         try {
             Object response = mClient.rpcCall("eventWait", mTimeout);
             if (response == null) {
-                CLog.e("response is null");
                 return true;
             }
             EventSl4aObject event = new EventSl4aObject(new JSONObject(response.toString()));
diff --git a/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java b/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
index d87b26b..a9b63a4 100644
--- a/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
+++ b/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.tradefed.util.sl4a;
 
+import static org.junit.Assert.*;
+
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.util.IRunUtil;
@@ -26,6 +28,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.io.File;
 import java.io.IOException;
 
 /**
@@ -121,6 +124,7 @@
                 + "com.googlecode.android_scripting");
         mMockRunUtil.sleep(EasyMock.anyLong());
         EasyMock.expectLastCall();
+        EasyMock.expect(mMockDevice.executeShellCommand(Sl4aClient.STOP_SL4A_CMD)).andReturn("");
         EasyMock.expect(mMockDevice.executeAdbCommand("forward", "tcp:" + mDeviceServer.getPort(),
                 "tcp:" + mDeviceServer.getPort())).andReturn("");
         EasyMock.expect(mMockDevice.executeAdbCommand("forward", "--list")).andReturn("");
@@ -162,4 +166,22 @@
         }
         EasyMock.verify(mMockDevice, mMockRunUtil);
     }
+
+    /**
+     * Test for {@link Sl4aClient#startSL4A(ITestDevice, File)} throws an exception if sl4a apk
+     * provided does not exist.
+     */
+    @Test
+    public void testCreateSl4aClient() throws Exception {
+        final String fakePath = "/fake/random/path";
+        EasyMock.replay(mMockDevice);
+        try {
+            Sl4aClient.startSL4A(mMockDevice, new File(fakePath));
+            fail("Should have thrown an exception");
+        } catch (RuntimeException expected) {
+            assertEquals(String.format("Sl4A apk '%s' was not found.", fakePath),
+                    expected.getMessage());
+        }
+        EasyMock.verify(mMockDevice);
+    }
 }