Add support for ConnectThread

ConnectThread runs the blocking call to BluetoothSocket#connect() until
it either succeeds -- at which point it creates a client network (TODO)
-- or is aborted because the request went away.

Any error encountered during BluetoothSocket#connect() triggers an
onUnavailable() on all requests matching that specifier. There are
currently no retries.

Test: TH
Change-Id: Ib199663c881610988fd587446337f9701c89de83
diff --git a/service/src/com/android/server/L2capNetworkProvider.java b/service/src/com/android/server/L2capNetworkProvider.java
index 32ee397..6948e58 100644
--- a/service/src/com/android/server/L2capNetworkProvider.java
+++ b/service/src/com/android/server/L2capNetworkProvider.java
@@ -37,6 +37,7 @@
 
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.BluetoothServerSocket;
 import android.bluetooth.BluetoothSocket;
@@ -275,6 +276,14 @@
         return new L2capNetwork(mHandler, mContext, mProvider, ifname, socket, tunFd, caps, cb);
     }
 
+    private static void closeBluetoothSocket(BluetoothSocket socket) {
+        try {
+            socket.close();
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to close BluetoothSocket", e);
+        }
+    }
+
     private class ReservedServerOffer implements NetworkOfferCallback {
         private final NetworkCapabilities mReservedCapabilities;
         private final AcceptThread mAcceptThread;
@@ -299,14 +308,6 @@
                 });
             }
 
-            private static void closeBluetoothSocket(BluetoothSocket socket) {
-                try {
-                    socket.close();
-                } catch (IOException e) {
-                    Log.w(TAG, "Failed to close BluetoothSocket", e);
-                }
-            }
-
             private void postCreateServerNetwork(BluetoothSocket connectedSocket) {
                 mHandler.post(() -> {
                     final boolean success = createServerNetwork(connectedSocket);
@@ -432,13 +433,70 @@
          * State object to store information for client NetworkRequests.
          */
         private static class ClientRequestInfo {
+            public final L2capNetworkSpecifier specifier;
             public final List<NetworkRequest> requests = new ArrayList<>();
+            // TODO: add support for retries.
+            public final ConnectThread connectThread;
 
-            public ClientRequestInfo(NetworkRequest request) {
-                requests.add(request);
+            public ClientRequestInfo(NetworkRequest request, ConnectThread connectThread) {
+                this.specifier = (L2capNetworkSpecifier) request.getNetworkSpecifier();
+                this.requests.add(request);
+                this.connectThread = connectThread;
             }
         }
 
+        // TODO: consider using ExecutorService
+        private class ConnectThread extends Thread {
+            private final L2capNetworkSpecifier mSpecifier;
+            private final BluetoothSocket mSocket;
+            private volatile boolean mIsAborted = false;
+
+            public ConnectThread(L2capNetworkSpecifier specifier, BluetoothSocket socket) {
+                mSpecifier = specifier;
+                mSocket = socket;
+            }
+
+            public void run() {
+                try {
+                    mSocket.connect();
+                    mHandler.post(() -> {
+                        final boolean success = createClientNetwork(mSpecifier, mSocket);
+                        if (!success) closeBluetoothSocket(mSocket);
+                    });
+                } catch (IOException e) {
+                    Log.e(TAG, "Failed to connect", e);
+                    if (mIsAborted) return;
+
+                    closeBluetoothSocket(mSocket);
+                    mHandler.post(() -> {
+                        declareAllNetworkRequestsUnfulfillable(mSpecifier);
+                    });
+                }
+            }
+
+            public void abort() {
+                mIsAborted = true;
+                // Closing the BluetoothSocket is the only way to unblock connect() because it calls
+                // shutdown on the underlying (connected) SOCK_SEQPACKET.
+                // It is safe to call BluetoothSocket#close() multiple times.
+                closeBluetoothSocket(mSocket);
+                try {
+                    join();
+                } catch (InterruptedException e) {
+                    Log.i(TAG, "Interrupted while joining ConnectThread", e);
+                }
+            }
+        }
+
+        private boolean createClientNetwork(L2capNetworkSpecifier specifier,
+                BluetoothSocket socket) {
+            // Check whether request still exists
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return false;
+
+            // TODO: implement createClientNetwork
+            return false;
+        }
 
         private boolean isValidL2capSpecifier(@Nullable NetworkSpecifier spec) {
             if (spec == null) return false;
@@ -501,9 +559,28 @@
             }
 
             // If the code reaches here, this is a new request.
-            mClientNetworkRequests.put(requestSpecifier, new ClientRequestInfo(request));
+            final BluetoothAdapter bluetoothAdapter = mBluetoothManager.getAdapter();
+            if (bluetoothAdapter == null) {
+                Log.w(TAG, "Failed to get BluetoothAdapter");
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
 
-            // TODO: implement onNetworkNeeded
+            final byte[] macAddress = requestSpecifier.getRemoteAddress().toByteArray();
+            final BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress);
+            final BluetoothSocket socket;
+            try {
+                socket = bluetoothDevice.createInsecureL2capChannel(requestSpecifier.getPsm());
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to createInsecureL2capChannel", e);
+                mProvider.declareNetworkRequestUnfulfillable(request);
+                return;
+            }
+
+            final ConnectThread connectThread = new ConnectThread(requestSpecifier, socket);
+            connectThread.start();
+            final ClientRequestInfo newRequestInfo = new ClientRequestInfo(request, connectThread);
+            mClientNetworkRequests.put(requestSpecifier, newRequestInfo);
         }
 
         @Override
@@ -519,9 +596,33 @@
             if (cri.requests.size() > 0) return;
 
             // If the code reaches here, the network needs to be torn down.
-            mClientNetworkRequests.remove(specifier);
+            releaseClientNetworkRequest(cri);
+        }
 
-            // TODO: implement onNetworkUnneeded
+        /**
+         * Release the client network request and tear down all associated state.
+         *
+         * Only call this when all associated NetworkRequests have been released.
+         */
+        private void releaseClientNetworkRequest(ClientRequestInfo cri) {
+            mClientNetworkRequests.remove(cri.specifier);
+            if (cri.connectThread.isAlive()) {
+                // Note that if ConnectThread succeeds between calling #isAlive() and #abort(), the
+                // request will already be removed from mClientNetworkRequests by the time the
+                // createClientNetwork() call is processed on the handler, so it is safe to call
+                // #abort().
+                cri.connectThread.abort();
+            }
+        }
+
+        private void declareAllNetworkRequestsUnfulfillable(L2capNetworkSpecifier specifier) {
+            final ClientRequestInfo cri = mClientNetworkRequests.get(specifier);
+            if (cri == null) return;
+
+            for (NetworkRequest request : cri.requests) {
+                mProvider.declareNetworkRequestUnfulfillable(request);
+            }
+            releaseClientNetworkRequest(cri);
         }
     }