Add rootcanal test and HCI channel communication facilities.

Rootcanal listens on TCP sockets for test commands and HCI commands.
The test channel is for adding/managing pre-made devices, and the HCI
channel is to control a custom H4 HCI device via direct HCI commands.

This exposes those to STS tests and allow easy communication via those.

Also automated port management so that there is no host-side port
collision when running a test on multiple devices at once.

Test: custom sts test that creates a device, adds it to the controller,
      then list devices
Bug: 216394844
Change-Id: I8cec46c9efab7aca824a1b3a0343499fd23cadd1
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
index 3849b95..c76ed5e 100644
--- a/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/RootcanalUtils.java
@@ -16,14 +16,23 @@
 
 package com.android.sts.common;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
 import static com.android.sts.common.CommandUtil.runAndCheck;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileWriter;
+import java.io.InputStream;
 import java.io.IOException;
 import java.io.StringReader;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
 import java.util.List;
@@ -74,34 +83,31 @@
         assertNotNull("Device not set", device);
         try {
             device.enableAdbRoot();
+            ProcessUtil.killAll(
+                    device, "android\\.hardware\\.bluetooth@1\\.1-service\\.sim", 10_000, false);
             runAndCheck(device, String.format("rm -rf '%s'", LOCK_FILENAME));
             device.disableAdbRoot();
             // OverlayFsUtils' finished() will restart the device.
             overlayFsUtils.finished(d);
+            runAndCheck(device, "svc bluetooth enable");
         } catch (DeviceNotAvailableException e) {
             throw new AssertionError("Device unavailable when cleaning up", e);
+        } catch (TimeoutException e) {
+            CLog.w("Could not kill rootcanal HAL during cleanup");
         }
     }
 
-    /** Replace existing HAL with RootCanal HAL on current device. */
-    public void enableRootcanal()
-            throws DeviceNotAvailableException, IOException, InterruptedException,
-                    TimeoutException {
-        enableRootcanal(6111);
-    }
-
     /**
      * Replace existing HAL with RootCanal HAL on current device.
      *
-     * @param port host TCP port to adb-forward to rootcanal control port.
+     * @return an instance of RootcanalController
      */
-    public void enableRootcanal(int port)
+    public RootcanalController enableRootcanal()
             throws DeviceNotAvailableException, IOException, InterruptedException,
                     TimeoutException {
         ITestDevice device = test.getDevice();
-        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
         assertNotNull("Device not set", device);
-        assertNotNull("Build not set", buildHelper);
+        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(test.getBuild());
 
         // Check and made sure we're not calling this more than once for a device
         assertFalse("rootcanal set up called more than once", device.doesFileExist(LOCK_FILENAME));
@@ -202,7 +208,14 @@
             }
         }
 
-        device.executeAdbCommand("forward", "tcp:6111", String.format("tcp:%d", port));
+        // Forward root canal control ports on the device to the host
+        int testPort = findOpenPort();
+        device.executeAdbCommand("forward", String.format("tcp:%d", testPort), "tcp:6111");
+
+        int hciPort = findOpenPort();
+        device.executeAdbCommand("forward", String.format("tcp:%d", hciPort), "tcp:6211");
+
+        return new RootcanalController(testPort, hciPort);
     }
 
     private void tryUpdateVintfManifest(ITestDevice device)
@@ -269,4 +282,182 @@
             TimeUnit.MILLISECONDS.sleep(250);
         }
     }
+
+    /** Find an open TCP port on the host */
+    private static int findOpenPort() throws IOException {
+        try (ServerSocket socket = new ServerSocket(0)) {
+            socket.setReuseAddress(true);
+            return socket.getLocalPort();
+        }
+    }
+
+    /** Class that encapsulates a virtual HCI device that can be controlled by HCI commands. */
+    public static class HciDevice implements AutoCloseable {
+        private static final String READ_FAIL_MSG = "Failed to read HCI packet";
+        private final Socket hciSocket;
+
+        private HciDevice(Socket hciSocket) {
+            this.hciSocket = hciSocket;
+        }
+
+        @Override
+        public void close() throws IOException {
+            hciSocket.close();
+        }
+
+        /**
+         * Convenient wrapper around sendHciPacket to send a HCI command packet to device.
+         *
+         * @param ogf Opcode group field
+         * @param ocf Opcode command field
+         * @param params the rest of the command parameters
+         */
+        public void sendHciCmd(int ogf, int ocf, byte[] params) throws IOException {
+            assertTrue("params length must be less than 256 bytes", params.length < 256);
+            ByteBuffer cmd = ByteBuffer.allocate(4 + params.length).order(ByteOrder.LITTLE_ENDIAN);
+            int opcode = (ogf << 10) | ocf;
+            cmd.put((byte) 0x01).putShort((short) opcode).put((byte) params.length).put(params);
+            sendHciPacket(cmd.array());
+        }
+
+        /**
+         * Send raw HCI packet to device.
+         *
+         * @param packet raw packet data to send to device
+         */
+        public void sendHciPacket(byte[] packet) throws IOException {
+            hciSocket.getOutputStream().write(packet);
+        }
+
+        /** Read one HCI packet from device, blocking until data is available. */
+        public byte[] readHciPacket() throws IOException {
+            ByteArrayOutputStream ret = new ByteArrayOutputStream();
+            InputStream in = hciSocket.getInputStream();
+
+            // Read the packet type
+            byte[] typeBuf = new byte[1];
+            assertEquals(READ_FAIL_MSG, 1, in.read(typeBuf, 0, 1));
+            ret.write(typeBuf);
+
+            // Read the header and figure out how much data to read
+            // according to BT core spec 5.2 vol 4 part A section 2 & part E section 5.4
+            byte[] hdrBuf = new byte[4];
+            int dataLength;
+
+            switch (typeBuf[0]) {
+                case 0x01: // Command packet
+                case 0x03: // Synch data packet
+                    assertEquals(READ_FAIL_MSG, 3, in.read(hdrBuf, 0, 3));
+                    ret.write(hdrBuf, 0, 3);
+                    dataLength = hdrBuf[2];
+                    break;
+
+                case 0x02: // Async data packet
+                    assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4));
+                    ret.write(hdrBuf, 0, 4);
+                    dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFF) << 8);
+                    break;
+
+                case 0x04: // Event
+                    assertEquals(READ_FAIL_MSG, 2, in.read(hdrBuf, 0, 2));
+                    ret.write(hdrBuf, 0, 2);
+                    dataLength = hdrBuf[1];
+                    break;
+
+                case 0x05: // ISO synch data packet
+                    assertEquals(READ_FAIL_MSG, 4, in.read(hdrBuf, 0, 4));
+                    ret.write(hdrBuf, 0, 4);
+                    dataLength = (((int) hdrBuf[2]) & 0xFF) | ((((int) hdrBuf[3]) & 0xFC) << 6);
+                    break;
+
+                default:
+                    throw new IOException("Unexpected packet type: " + String.valueOf(typeBuf[0]));
+            }
+
+            // Read the data payload
+            byte[] data = new byte[dataLength];
+            assertEquals(READ_FAIL_MSG, dataLength, in.read(data, 0, dataLength));
+            ret.write(data, 0, dataLength);
+
+            return ret.toByteArray();
+        }
+    }
+
+    public static class RootcanalController implements AutoCloseable {
+        private final int testPort;
+        private final int hciPort;
+        private Socket rootcanalTestChannel = null;
+        private List<HciDevice> hciDevices = new ArrayList<>();
+
+        private RootcanalController(int testPort, int hciPort)
+                throws IOException, InterruptedException {
+            this.testPort = testPort;
+            this.hciPort = hciPort;
+            CLog.d("Rootcanal controller initialized; testPort=%d, hciPort=%d", testPort, hciPort);
+        }
+
+        @Override
+        public void close() throws IOException {
+            rootcanalTestChannel.close();
+            for (HciDevice dev : hciDevices) {
+                dev.close();
+            }
+        }
+
+        /**
+         * Create a new HCI device by connecting to rootcanal's HCI socket.
+         *
+         * @return HciDevice object that allows sending/receiving from the HCI port
+         */
+        public HciDevice createHciDevice()
+                throws DeviceNotAvailableException, IOException, InterruptedException {
+            HciDevice dev = new HciDevice(new Socket("localhost", hciPort));
+            hciDevices.add(dev);
+            return dev;
+        }
+
+        /**
+         * Send one command to rootcanal test channel.
+         *
+         * <p>Send `help` command for list of accepted commands from Rootcanal.
+         *
+         * @param cmd command to send
+         * @param args arguments for the command
+         * @return Response string from rootcanal
+         */
+        public String sendTestChannelCommand(String cmd, String... args)
+                throws IOException, InterruptedException {
+            if (rootcanalTestChannel == null) {
+                rootcanalTestChannel = new Socket("localhost", testPort);
+                CLog.d(
+                        "RootCanal test channel init: "
+                                + readTestChannel(rootcanalTestChannel.getInputStream()));
+            }
+
+            // Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py
+            ByteArrayOutputStream msg = new ByteArrayOutputStream();
+            msg.write(cmd.length());
+            msg.write(cmd.getBytes("ASCII"));
+            msg.write(args.length);
+            for (String arg : args) {
+                msg.write(arg.length());
+                msg.write(arg.getBytes("ASCII"));
+            }
+
+            rootcanalTestChannel.getOutputStream().write(msg.toByteArray());
+            return readTestChannel(rootcanalTestChannel.getInputStream());
+        }
+
+        /** Read one message from rootcanal test channel. */
+        private String readTestChannel(InputStream in) throws IOException {
+            // Translated from system/bt/vendor_libs/test_vendor_lib/scripts/test_channel.py
+            ByteBuffer sizeBuf = ByteBuffer.allocate(Integer.BYTES).order(ByteOrder.LITTLE_ENDIAN);
+            in.read(sizeBuf.array(), 0, Integer.BYTES);
+            int size = sizeBuf.getInt();
+
+            byte[] buf = new byte[size];
+            in.read(buf, 0, size);
+            return new String(buf);
+        }
+    }
 }