Implement connection handover LLCP service.

The Connection Handover specification dictates that we should
exchange Hr/Hs records over a dedicated LLCP service. This adds
the service, and starts using it by default. It will fall back
to the SNEP GET method to remain compatible with Android 4.1
devices. SNEP GET on these devices will return "Not Implemented",
also per the SNEP spec.

Also fixed a bug in endianness of the OOB record.

Bug: 6759842
Change-Id: Ifd360f556bf0e1757eb8bbaadf11efa094aefcf6
diff --git a/src/com/android/nfc/P2pLinkManager.java b/src/com/android/nfc/P2pLinkManager.java
index 189b560..253ddaf 100755
--- a/src/com/android/nfc/P2pLinkManager.java
+++ b/src/com/android/nfc/P2pLinkManager.java
@@ -17,7 +17,9 @@
 package com.android.nfc;
 
 import com.android.nfc.echoserver.EchoServer;
+import com.android.nfc.handover.HandoverClient;
 import com.android.nfc.handover.HandoverManager;
+import com.android.nfc.handover.HandoverServer;
 import com.android.nfc.ndefpush.NdefPushClient;
 import com.android.nfc.ndefpush.NdefPushServer;
 import com.android.nfc.snep.SnepClient;
@@ -123,6 +125,7 @@
 
     // TODO dynamically assign SAP values
     static final int NDEFPUSH_SAP = 0x10;
+    static final int HANDOVER_SAP = 0x14;
 
     static final int LINK_DEBOUNCE_MS = 750;
 
@@ -155,6 +158,7 @@
 
     final NdefPushServer mNdefPushServer;
     final SnepServer mDefaultSnepServer;
+    final HandoverServer mHandoverServer;
     final EchoServer mEchoServer;
     final ActivityManager mActivityManager;
     final PackageManager mPackageManager;
@@ -178,6 +182,8 @@
     public P2pLinkManager(Context context, HandoverManager handoverManager) {
         mNdefPushServer = new NdefPushServer(NDEFPUSH_SAP, mNppCallback);
         mDefaultSnepServer = new SnepServer(mDefaultSnepCallback);
+        mHandoverServer = new HandoverServer(HANDOVER_SAP, handoverManager, mHandoverCallback);
+
         if (ECHOSERVER_ENABLED) {
             mEchoServer = new EchoServer();
         } else {
@@ -206,12 +212,14 @@
             if (!mIsReceiveEnabled && receiveEnable) {
                 mDefaultSnepServer.start();
                 mNdefPushServer.start();
+                mHandoverServer.start();
                 if (mEchoServer != null) {
                     mHandler.sendEmptyMessage(MSG_START_ECHOSERVER);
                 }
             } else if (mIsReceiveEnabled && !receiveEnable) {
                 mDefaultSnepServer.stop();
                 mNdefPushServer.stop();
+                mHandoverServer.stop();
                 if (mEchoServer != null) {
                     mHandler.sendEmptyMessage(MSG_STOP_ECHOSERVER);
                 }
@@ -465,11 +473,20 @@
 
         try {
             if (uris != null) {
+                HandoverClient handoverClient = new HandoverClient();
+
                 NdefMessage response = null;
                 NdefMessage request = handoverManager.createHandoverRequestMessage();
                 if (request != null) {
-                    SnepMessage snepResponse = snepClient.get(request);
-                    response = snepResponse.getNdefMessage();
+                    response = handoverClient.sendHandoverRequest(request);
+
+                    if (response == null) {
+                        // Remote device may not support handover service,
+                        // try the (deprecated) SNEP GET implementation
+                        // for devices running Android 4.1
+                        SnepMessage snepResponse = snepClient.get(request);
+                        response = snepResponse.getNdefMessage();
+                    }
                 } // else, handover not supported
                 if (response != null) {
                     handoverManager.doHandoverUri(uris, response);
@@ -495,6 +512,13 @@
         return SNEP_FAILURE;
     }
 
+    final HandoverServer.Callback mHandoverCallback = new HandoverServer.Callback() {
+        @Override
+        public void onHandoverRequestReceived() {
+            onReceiveHandover();
+        }
+    };
+
     final NdefPushServer.Callback mNppCallback = new NdefPushServer.Callback() {
         @Override
         public void onMessageReceived(NdefMessage msg) {
@@ -511,13 +535,17 @@
 
         @Override
         public SnepMessage doGet(int acceptableLength, NdefMessage msg) {
+            // The NFC Forum Default SNEP server is not allowed to respond to
+            // SNEP GET requests - see SNEP 1.0 TS section 6.1. However,
+            // since Android 4.1 used the NFC Forum default server to
+            // implement connection handover, we will support this
+            // until we can deprecate it.
             NdefMessage response = mHandoverManager.tryHandoverRequest(msg);
-
             if (response != null) {
                 onReceiveHandover();
                 return SnepMessage.getSuccessResponse(response);
             } else {
-                return SnepMessage.getMessage(SnepMessage.RESPONSE_NOT_FOUND);
+                return SnepMessage.getMessage(SnepMessage.RESPONSE_NOT_IMPLEMENTED);
             }
         }
     };
diff --git a/src/com/android/nfc/handover/BluetoothOppHandover.java b/src/com/android/nfc/handover/BluetoothOppHandover.java
index ece6a7b..ceb3c62 100644
--- a/src/com/android/nfc/handover/BluetoothOppHandover.java
+++ b/src/com/android/nfc/handover/BluetoothOppHandover.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.
+ */
+
 package com.android.nfc.handover;
 
 import android.bluetooth.BluetoothAdapter;
@@ -33,7 +49,7 @@
 
     static final int MSG_START_SEND = 0;
 
-    static final int REMOTE_BT_ENABLE_DELAY_MS = 3000;
+    static final int REMOTE_BT_ENABLE_DELAY_MS = 5000;
 
     static final String ACTION_HANDOVER_SEND =
             "android.btopp.intent.action.HANDOVER_SEND";
diff --git a/src/com/android/nfc/handover/ConfirmConnectActivity.java b/src/com/android/nfc/handover/ConfirmConnectActivity.java
index 22d518f..35a7da1 100644
--- a/src/com/android/nfc/handover/ConfirmConnectActivity.java
+++ b/src/com/android/nfc/handover/ConfirmConnectActivity.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.
+ */
+
 package com.android.nfc.handover;
 
 import android.app.Activity;
diff --git a/src/com/android/nfc/handover/HandoverClient.java b/src/com/android/nfc/handover/HandoverClient.java
new file mode 100644
index 0000000..86cd23b
--- /dev/null
+++ b/src/com/android/nfc/handover/HandoverClient.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.nfc.handover;
+
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.util.Log;
+
+import com.android.nfc.LlcpException;
+import com.android.nfc.NfcService;
+import com.android.nfc.DeviceHost.LlcpSocket;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+public final class HandoverClient {
+    private static final String TAG = "HandoverClient";
+    private static final int MIU = 128;
+    // Max NDEF length to receive for Hr/Hs messages
+    private static final boolean DBG = false;
+
+    public NdefMessage sendHandoverRequest(NdefMessage msg) {
+        if (msg == null) return null;
+
+        NfcService service = NfcService.getInstance();
+
+        LlcpSocket sock = null;
+        int offset = 0;
+        byte[] buffer = msg.toByteArray();
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+
+        try {
+            sock = service.createLlcpSocket(0, MIU, 1, 1024);
+            if (sock == null) {
+                throw new IOException("Could not connect to socket.");
+            }
+            if (DBG) Log.d(TAG, "about to connect to service " +
+                    HandoverServer.HANDOVER_SERVICE_NAME);
+            sock.connectToService(HandoverServer.HANDOVER_SERVICE_NAME);
+
+            int remoteMiu = sock.getRemoteMiu();
+            if (DBG) Log.d(TAG, "about to send a " + buffer.length + " byte message");
+            while (offset < buffer.length) {
+                int length = Math.min(buffer.length - offset, remoteMiu);
+                byte[] tmpBuffer = Arrays.copyOfRange(buffer, offset, offset+length);
+                if (DBG) Log.d(TAG, "about to send a " + length + " byte packet");
+                sock.send(tmpBuffer);
+                offset += length;
+            }
+
+            // Now, try to read back the SNEP response
+            byte[] partial = new byte[sock.getLocalMiu()];
+            NdefMessage handoverSelectMsg = null;
+            while (true) {
+                int size = sock.receive(partial);
+                if (size < 0) {
+                    break;
+                }
+                byteStream.write(partial, 0, size);
+                try {
+                    handoverSelectMsg = new NdefMessage(byteStream.toByteArray());
+                    // If we get here, message is complete
+                    break;
+                } catch (FormatException e) {
+                    // Ignore, and try to fetch more bytes
+                }
+            }
+            return handoverSelectMsg;
+        } catch (IOException e) {
+            if (DBG) Log.d(TAG, "couldn't connect to handover service");
+        } catch (LlcpException e) {
+            if (DBG) Log.d(TAG, "couldn't connect to handover service");
+        } finally {
+            if (sock != null) {
+                try {
+                    if (DBG) Log.d(TAG, "about to close");
+                    sock.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+            try {
+                byteStream.close();
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java
index 9836cdc..51b123c 100644
--- a/src/com/android/nfc/handover/HandoverManager.java
+++ b/src/com/android/nfc/handover/HandoverManager.java
@@ -657,8 +657,12 @@
 
     NdefRecord createBluetoothOobDataRecord() {
         byte[] payload = new byte[8];
-        payload[0] = 0;
-        payload[1] = (byte)payload.length;
+        // Note: this field should be little-endian per the BTSSP spec
+        // The Android 4.1 implementation used big-endian order here.
+        // No single Android implementation has ever interpreted this
+        // length field when parsing this record though.
+        payload[0] = (byte) (payload.length & 0xFF);
+        payload[1] = (byte) ((payload.length >> 8) & 0xFF);
 
         synchronized (HandoverManager.this) {
             if (mLocalBluetoothAddress == null) {
diff --git a/src/com/android/nfc/handover/HandoverServer.java b/src/com/android/nfc/handover/HandoverServer.java
new file mode 100644
index 0000000..e789387
--- /dev/null
+++ b/src/com/android/nfc/handover/HandoverServer.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * 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.
+ */
+package com.android.nfc.handover;
+
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.util.Log;
+
+import com.android.nfc.LlcpException;
+import com.android.nfc.NfcService;
+import com.android.nfc.DeviceHost.LlcpServerSocket;
+import com.android.nfc.DeviceHost.LlcpSocket;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+public final class HandoverServer {
+    public static final String HANDOVER_SERVICE_NAME = "urn:nfc:sn:handover";
+    public static final String TAG = "HandoverServer";
+    public static final Boolean DBG = false;
+
+    public static final int MIU = 128;
+
+    final HandoverManager mHandoverManager;
+    final int mSap;
+    final Callback mCallback;
+
+    ServerThread mServerThread = null;
+    boolean mServerRunning = false;
+
+    public interface Callback {
+        void onHandoverRequestReceived();
+    }
+
+    public HandoverServer(int sap, HandoverManager manager, Callback callback) {
+        mSap = sap;
+        mHandoverManager = manager;
+        mCallback = callback;
+    }
+
+    public synchronized void start() {
+        if (mServerThread == null) {
+            mServerThread = new ServerThread();
+            mServerThread.start();
+            mServerRunning = true;
+        }
+    }
+
+    public synchronized void stop() {
+        if (mServerThread != null) {
+            mServerThread.shutdown();
+            mServerThread = null;
+            mServerRunning = false;
+        }
+    }
+
+    private class ServerThread extends Thread {
+        private boolean mThreadRunning = true;
+        LlcpServerSocket mServerSocket;
+
+        @Override
+        public void run() {
+            boolean threadRunning;
+            synchronized (HandoverServer.this) {
+                threadRunning = mThreadRunning;
+            }
+
+            while (threadRunning) {
+                try {
+                    synchronized (HandoverServer.this) {
+                        mServerSocket = NfcService.getInstance().createLlcpServerSocket(mSap,
+                                HANDOVER_SERVICE_NAME, MIU, 1, 1024);
+                    }
+                    if (mServerSocket == null) {
+                        if (DBG) Log.d(TAG, "failed to create LLCP service socket");
+                        return;
+                    }
+                    if (DBG) Log.d(TAG, "created LLCP service socket");
+                    synchronized (HandoverServer.this) {
+                        threadRunning = mThreadRunning;
+                    }
+
+                    while (threadRunning) {
+                        LlcpServerSocket serverSocket;
+                        synchronized (HandoverServer.this) {
+                            serverSocket = mServerSocket;
+                        }
+
+                        if (serverSocket == null) {
+                            if (DBG) Log.d(TAG, "Server socket shut down.");
+                            return;
+                        }
+                        if (DBG) Log.d(TAG, "about to accept");
+                        LlcpSocket communicationSocket = serverSocket.accept();
+                        if (DBG) Log.d(TAG, "accept returned " + communicationSocket);
+                        if (communicationSocket != null) {
+                            new ConnectionThread(communicationSocket).start();
+                        }
+
+                        synchronized (HandoverServer.this) {
+                            threadRunning = mThreadRunning;
+                        }
+                    }
+                    if (DBG) Log.d(TAG, "stop running");
+                } catch (LlcpException e) {
+                    Log.e(TAG, "llcp error", e);
+                } catch (IOException e) {
+                    Log.e(TAG, "IO error", e);
+                } finally {
+                    synchronized (HandoverServer.this) {
+                        if (mServerSocket != null) {
+                            if (DBG) Log.d(TAG, "about to close");
+                            try {
+                                mServerSocket.close();
+                            } catch (IOException e) {
+                                // ignore
+                            }
+                            mServerSocket = null;
+                        }
+                    }
+                }
+
+                synchronized (HandoverServer.this) {
+                    threadRunning = mThreadRunning;
+                }
+            }
+        }
+
+        public void shutdown() {
+            synchronized (HandoverServer.this) {
+                mThreadRunning = false;
+                if (mServerSocket != null) {
+                    try {
+                        mServerSocket.close();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                    mServerSocket = null;
+                }
+            }
+        }
+    }
+
+    private class ConnectionThread extends Thread {
+        private final LlcpSocket mSock;
+
+        ConnectionThread(LlcpSocket socket) {
+            super(TAG);
+            mSock = socket;
+        }
+
+        @Override
+        public void run() {
+            if (DBG) Log.d(TAG, "starting connection thread");
+            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+
+            try {
+                boolean running;
+                synchronized (HandoverServer.this) {
+                    running = mServerRunning;
+                }
+
+                byte[] partial = new byte[mSock.getLocalMiu()];
+
+                NdefMessage handoverRequestMsg = null;
+                while (running) {
+                    int size = mSock.receive(partial);
+                    if (size < 0) {
+                        break;
+                    }
+                    byteStream.write(partial, 0, size);
+                    // 1) Try to parse a handover request message from bytes received so far
+                    try {
+                        handoverRequestMsg = new NdefMessage(byteStream.toByteArray());
+                    } catch (FormatException e) {
+                        // Ignore, and try to fetch more bytes
+                    }
+
+                    if (handoverRequestMsg != null) {
+                        // 2) convert to handover response
+                        NdefMessage resp = mHandoverManager.tryHandoverRequest(handoverRequestMsg);
+                        if (resp == null) {
+                            Log.e(TAG, "Failed to create handover response");
+                            break;
+                        }
+
+                        // 3) send handover response
+                        int offset = 0;
+                        byte[] buffer = resp.toByteArray();
+                        int remoteMiu = mSock.getRemoteMiu();
+                        while (offset < buffer.length) {
+                            int length = Math.min(buffer.length - offset, remoteMiu);
+                            byte[] tmpBuffer = Arrays.copyOfRange(buffer, offset, offset+length);
+                            mSock.send(tmpBuffer);
+                            offset += length;
+                        }
+                        // We're done
+                        mCallback.onHandoverRequestReceived();
+                        break;
+                    }
+
+                    synchronized (HandoverServer.this) {
+                        running = mServerRunning;
+                    }
+                }
+
+            } catch (IOException e) {
+                if (DBG) Log.d(TAG, "IOException");
+            } finally {
+                try {
+                    if (DBG) Log.d(TAG, "about to close");
+                    mSock.close();
+                } catch (IOException e) {
+                    // ignore
+                }
+                try {
+                    byteStream.close();
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+            if (DBG) Log.d(TAG, "finished connection thread");
+        }
+    }
+}