rpc/bt: fix bt socket read

  * Fix BT socket read() to read until the specified number of bytes are
    read or fails.

  * Change GattReader to use AUTO for all device type since it doesn't
    seem to help.

MultiPart: 1/2

Change-Id: I31c4ba5566c10f8f0fda6f0e92261231b028468f
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java
index c867b42..6503735 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/Driver.java
@@ -397,7 +397,7 @@
             mLeScanner.stopScan(mLeScanCallback);
             mLeScanCallback = null;
         }
-        mGattReader.close();
+        mGattReader.close(true);
         mGattReader = null;
         mScanSeens = null;
     }
@@ -414,11 +414,10 @@
         mClassicScanner.close();
         mClassicScanner = null;
         if (mScanHandler != null) {
-            mGattReader.close();
-            mGattReader = null;
-
             // mLeScanner is invalidated when BluetoothAdapter is turned off.
-            // We don't need to stop any active scan.
+            // We don't need to stop any active scan or Gatt read.
+            mGattReader.close(false);
+            mGattReader = null;
             mLeScanner = null;
             mLeScanCallback = null;
             mScanSeens = null;
diff --git a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java
index 921fef0..ebc87a0 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/discovery/plugins/ble/GattReader.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.Queues;
 
+import java.lang.reflect.Method;
 import java.util.ArrayDeque;
 import java.util.Iterator;
 import java.util.Set;
@@ -73,6 +74,7 @@
     GattReader(Context context, Set<UUID> uuids, UUID baseUuid, UUID maskUuid, Handler handler) {
         mContext = context;
         mExecutor = new ScheduledThreadPoolExecutor(1);
+        mExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
         mScanUuids = uuids;
         mScanBaseUuid = baseUuid;
         mScanMaskUuid = maskUuid;
@@ -96,12 +98,22 @@
     /**
      * Closes the Gatt reader cancelling the current read and deleting all pending requests.
      */
-    synchronized void close() {
+    synchronized void close(boolean graceful) {
+        mPendingReads.clear();
+        if (graceful) {
+            // Wait until the current read finishes to avoid messing up the Bluetooth stack.
+            while (mCurrentDevice != null) {
+                try {
+                    wait();
+                } catch (InterruptedException e) {
+                    break;
+                }
+            }
+        }
+        mExecutor.shutdown();
         if (mCurrentGatt != null) {
             mCurrentGatt.close();
         }
-        mExecutor.shutdown();
-        mPendingReads.clear();
     }
 
     private synchronized void maybeReadNextDevice() {
@@ -113,18 +125,11 @@
 
         mCurrentDevice = mPendingReads.poll();
         if (mCurrentDevice == null) {
+            notifyAll();
             return;
         }
 
-        if ((mCurrentDevice.getType() == BluetoothDevice.DEVICE_TYPE_CLASSIC
-                        || mCurrentDevice.getType() == BluetoothDevice.DEVICE_TYPE_DUAL)
-                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            mCurrentGatt =
-                    mCurrentDevice.connectGatt(
-                            mContext, false, this, BluetoothDevice.TRANSPORT_BREDR);
-        } else {
-            mCurrentGatt = mCurrentDevice.connectGatt(mContext, false, this);
-        }
+        mCurrentGatt = mCurrentDevice.connectGatt(mContext, false, this);
         mCurrentGattConnectionTimeout =
                 mExecutor.schedule(
                         new Runnable() {
@@ -147,6 +152,15 @@
 
     private synchronized void cancelAndMaybeReadNextDevice() {
         mCurrentGattConnectionTimeout.cancel(false);
+        // Try to refresh the failed Gatt.
+        try {
+            Method method = mCurrentGatt.getClass().getMethod("refresh", new Class[0]);
+            if (method != null) {
+                method.invoke(mCurrentGatt, new Object[0]);
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "An exception occured while refreshing device");
+        }
         mCurrentGatt.close();
 
         final BluetoothDevice device = mCurrentDevice;
@@ -181,6 +195,11 @@
             return;
         }
 
+        // TODO(jhahn): Do we really need this?
+        if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+            gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+        }
+
         // MTU exchange is not allowed on a BR/EDR physical link.
         // (Bluetooth Core Specification Volume 3, Part G, 4.3.1)
         //
diff --git a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
index 8b2af7b..87ed359 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
@@ -14,19 +14,17 @@
 
 import org.joda.time.Duration;
 
+import java.io.InputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.lang.reflect.Method;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Timer;
 import java.util.TimerTask;
-import java.util.concurrent.Executor;
 
-import io.v.impl.google.rt.VRuntimeImpl;
 import io.v.android.v23.V;
 import io.v.v23.context.VContext;
-import io.v.v23.rpc.Callback;
 import io.v.v23.verror.VException;
 
 /**
@@ -38,7 +36,7 @@
 class Bluetooth {
     private static final String TAG = "Bluetooth";
 
-    static Listener listen(VContext ctx, String btAddr) throws VException {
+    static Listener listen(VContext ctx, String btAddr) throws Exception {
         String macAddr = getMACAddress(ctx, btAddr);
         int port = getPortNumber(btAddr);
         BluetoothServerSocket socket = listenOnPort(port);
@@ -47,112 +45,79 @@
             port = getPortNumber(socket);
         }
         Log.d(TAG, String.format("listening on port %d", port));
-        Executor executor = VRuntimeImpl.getRuntimeExecutor(ctx);
-        if (executor == null) {
-            throw new VException(
-                    "NULL executor in context: did you derive this context from "
-                            + "the context returned by V.init()?");
-        }
-        return new Listener(executor, socket, String.format("%s/%d", macAddr, port));
+        return new Listener(socket, String.format("%s/%d", macAddr, port));
     }
 
-    static void dial(
-            VContext ctx, String btAddr, final Duration timeout, final Callback<Stream> callback)
-            throws VException {
-        final String macAddr = getMACAddress(ctx, btAddr);
-        final int port = getPortNumber(btAddr);
-        final Executor executor = VRuntimeImpl.getRuntimeExecutor(ctx);
-        if (executor == null) {
-            throw new VException(
-                    "NULL executor in context: did you derive this context from "
-                            + "the context returned by V.init()?");
-        }
-        executor.execute(
-                new Runnable() {
-                    @Override
-                    public void run() {
-                        final BluetoothDevice device =
-                                BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddr);
-                        try {
-                            // Create a socket to the remote device.
-                            // NOTE(spetrovic): Android's public methods currently only allow connection to
-                            // a UUID, which goes through SDP.  Since we already have a remote port number,
-                            // we connect to it directly, invoking a hidden method using reflection.
-                            Method m =
-                                    device.getClass()
-                                            .getMethod(
-                                                    "createInsecureRfcommSocket",
-                                                    new Class[] {int.class});
-                            final BluetoothSocket socket = (BluetoothSocket) m.invoke(device, port);
-                            // Connect.
-                            Timer timer = null;
-                            if (timeout.getMillis() != 0) {
-                                timer = new Timer();
-                                timer.schedule(
-                                        new TimerTask() {
-                                            @Override
-                                            public void run() {
-                                                try {
-                                                    socket.close();
-                                                } catch (IOException e) {
-                                                    System.err.println(
-                                                            "Couldn't close BluetoothSocket.");
-                                                }
-                                            }
-                                        },
-                                        timeout.getMillis());
-                            }
+    static Stream dial(VContext ctx, String btAddr, Duration timeout) throws Exception {
+        String macAddr = getMACAddress(ctx, btAddr);
+        int port = getPortNumber(btAddr);
+        BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddr);
+
+        // Create a socket to the remote device.
+        // NOTE(spetrovic): Android's public methods currently only allow connection to
+        // a UUID, which goes through SDP.  Since we already have a remote port number,
+        // we connect to it directly, invoking a hidden method using reflection.
+        Method m =
+                device.getClass().getMethod("createInsecureRfcommSocket", new Class[] {int.class});
+        final BluetoothSocket socket = (BluetoothSocket) m.invoke(device, port);
+        // Connect.
+        Timer timer = null;
+        if (timeout.getMillis() != 0) {
+            timer = new Timer();
+            timer.schedule(
+                    new TimerTask() {
+                        @Override
+                        public void run() {
                             try {
-                                socket.connect();
-                            } catch (IOException e) {
                                 socket.close();
-                                callback.onFailure(
-                                        new VException("Couldn't connect: " + e.getMessage()));
-                            } finally {
-                                if (timer != null) {
-                                    timer.cancel();
-                                }
+                            } catch (IOException e) {
                             }
-                            // There is no way currently to retrieve the local port number for the
-                            // connection, but that's probably OK.
-                            String localAddr = String.format("%s/%d", localMACAddress(ctx), 0);
-                            String remoteAddr = String.format("%s/%d", macAddr, port);
-                            callback.onSuccess(new Stream(executor, socket, localAddr, remoteAddr));
-                        } catch (Exception e) {
-                            callback.onFailure(
-                                    new VException(
-                                            "Couldn't invoke createInsecureRfcommSocket: "
-                                                    + e.getMessage()));
                         }
-                    }
-                });
+                    },
+                    timeout.getMillis());
+        }
+        try {
+            socket.connect();
+        } catch (IOException e) {
+            socket.close();
+            throw e;
+        } finally {
+            if (timer != null) {
+                timer.cancel();
+            }
+        }
+        // There is no way currently to retrieve the local port number for the
+        // connection, but that's probably OK.
+        String localAddr = String.format("%s/%d", localMACAddress(ctx), 0);
+        String remoteAddr = String.format("%s/%d", macAddr, port);
+        return new Stream(socket, localAddr, remoteAddr);
     }
 
-    private static BluetoothServerSocket listenOnPort(int port) throws VException {
+    private static BluetoothServerSocket listenOnPort(int port) throws Exception {
+        //  Note that Android developer guide says that unlike TCP/IP, RFCOMM only allows
+        //  one connected client per channel at a time:
+        //  https://developer.android.com/guide/topics/connectivity/bluetooth.html.
+        //  But this seems to be conflict with the android reference page.
+        //  https://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html#accept()
+        //
+        //  Multiple client connection on a same listening channel seem to work with some testing devices
+        //  like Nexus 6 or Nexus 9, but this is not guaranteed to work with other devices.
         if (port == 0) {
             // Use SOCKET_CHANNEL_AUTO_STATIC (-2) to auto assign a channel number.
             port = -2;
         }
         // Use reflection to reach the hidden "listenUsingInsecureRfcommOn(port)" method.
         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        try {
-            Method m =
-                    adapter.getClass()
-                            .getMethod("listenUsingInsecureRfcommOn", new Class[] {int.class});
-            return (BluetoothServerSocket) m.invoke(adapter, port);
-        } catch (Exception e) {
-            throw new VException("Error invoking listenUsingInsecureRfcommOn: " + e.getMessage());
-        }
+        Method m =
+                adapter.getClass()
+                        .getMethod("listenUsingInsecureRfcommOn", new Class[] {int.class});
+        return (BluetoothServerSocket) m.invoke(adapter, port);
     }
 
-    private static int getPortNumber(BluetoothServerSocket serverSocket) throws VException {
+    private static int getPortNumber(BluetoothServerSocket serverSocket) throws Exception {
         // Use reflection to reach the hidden "getChannel()" method.
-        try {
-            Method m = serverSocket.getClass().getMethod("getChannel", new Class[0]);
-            return (int) m.invoke(serverSocket);
-        } catch (Exception e) {
-            throw new VException("Error invoking getChannel: " + e.getMessage());
-        }
+        Method m = serverSocket.getClass().getMethod("getChannel", new Class[0]);
+        return (int) m.invoke(serverSocket);
     }
 
     private static String localMACAddress(VContext ctx) {
@@ -201,7 +166,7 @@
             case 2:
                 try {
                     int port = Integer.parseInt((parts.get(parts.size() - 1)));
-                    if (port < 0 || port > 32) {
+                    if (port < 0 || port > 30) {
                         throw new VException(
                                 String.format(
                                         "Illegal port number %q in bluetooth " + "address \"%s\".",
@@ -223,39 +188,26 @@
     }
 
     static class Listener {
-        private final Executor executor;
         private final BluetoothServerSocket serverSocket;
         private final String localAddress;
 
-        Listener(Executor executor, BluetoothServerSocket serverSocket, String address) {
-            this.executor = executor;
+        Listener(BluetoothServerSocket serverSocket, String address) {
             this.serverSocket = serverSocket;
             this.localAddress = address;
         }
 
-        void accept(final Callback<Stream> callback) {
-            executor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                BluetoothSocket socket = serverSocket.accept();
-                                // There is no way currently to retrieve the remote end's channel number,
-                                // but that's probably OK.
-                                String remoteAddress =
-                                        String.format(
-                                                "%s/%d", socket.getRemoteDevice().getAddress(), 0);
-                                callback.onSuccess(
-                                        new Stream(executor, socket, localAddress, remoteAddress));
-                            } catch (IOException e) {
-                                try {
-                                    serverSocket.close();
-                                } catch (IOException ioe) {
-                                }
-                                callback.onFailure(new VException(e.getMessage()));
-                            }
-                        }
-                    });
+        Stream accept() throws IOException {
+            try {
+                BluetoothSocket socket = serverSocket.accept();
+                // There is no way currently to retrieve the remote end's channel number,
+                // but that's probably OK.
+                String remoteAddress =
+                        String.format("%s/%d", socket.getRemoteDevice().getAddress(), 0);
+                return new Stream(socket, localAddress, remoteAddress);
+            } catch (IOException e) {
+                serverSocket.close();
+                throw e;
+            }
         }
 
         void close() throws IOException {
@@ -268,60 +220,45 @@
     }
 
     static class Stream {
-        private final Executor executor;
         private final BluetoothSocket socket;
         private final String localAddress;
         private final String remoteAddress;
 
-        Stream(
-                Executor executor,
-                BluetoothSocket socket,
-                String localAddress,
-                String remoteAddress) {
-            this.executor = executor;
+        Stream(BluetoothSocket socket, String localAddress, String remoteAddress) {
             this.socket = socket;
             this.localAddress = localAddress;
             this.remoteAddress = remoteAddress;
         }
 
-        void read(final int n, final Callback<byte[]> callback) {
-            executor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                byte[] buf = new byte[n];
-                                int num = socket.getInputStream().read(buf);
-                                callback.onSuccess(
-                                        num == buf.length ? buf : Arrays.copyOf(buf, num));
-                            } catch (IOException e) {
-                                try {
-                                    socket.close();
-                                } catch (IOException ioe) {
-                                }
-                                callback.onFailure(new VException(e.getMessage()));
-                            }
-                        }
-                    });
+        byte[] read(int n) throws IOException {
+            try {
+                InputStream in = socket.getInputStream();
+                byte[] buf = new byte[n];
+                int total = 0;
+                while (total < n) {
+                    int r = in.read(buf, total, n - total);
+                    if (r < 0) {
+                        break;
+                    }
+                    total += r;
+                }
+                return total == n ? buf : Arrays.copyOf(buf, total);
+            } catch (IOException e) {
+                socket.close();
+                throw e;
+            }
         }
 
-        void write(final byte[] data, final Callback<Void> callback) {
-            executor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                socket.getOutputStream().write(data);
-                                callback.onSuccess(null);
-                            } catch (IOException e) {
-                                try {
-                                    socket.close();
-                                } catch (IOException ioe) {
-                                }
-                                callback.onFailure(new VException(e.getMessage()));
-                            }
-                        }
-                    });
+        void write(byte[] data) throws IOException {
+            try {
+                OutputStream out = socket.getOutputStream();
+                out.write(data);
+                // TODO(jhahn): Do we need to flush for every write?
+                // out.flush();
+            } catch (IOException e) {
+                socket.close();
+                throw e;
+            }
         }
 
         void close() throws IOException {